From e15edeadb5c594da3ebc5885cad302bf1cba61a8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 29 Nov 2023 23:54:37 +0000 Subject: [PATCH 001/131] Revert "Fix issues with limits. (pull request #1089)" --- .../components/inventory-list/inventory-list.container.jsx | 5 ++--- client/src/components/jobs-list/jobs-list.component.jsx | 2 +- client/src/components/owners-list/owners-list.container.jsx | 5 ++--- .../src/components/vehicles-list/vehicles-list.container.jsx | 5 ++--- client/src/pages/bills/bills.page.container.jsx | 5 ++--- client/src/pages/contracts/contracts.page.container.jsx | 5 ++--- .../courtesy-car-detail.page.container.jsx | 5 ++--- client/src/pages/export-logs/export-logs.page.component.jsx | 4 ++-- client/src/pages/jobs-all/jobs-all.container.jsx | 5 ++--- .../src/pages/payments-all/payments-all.container.page.jsx | 5 ++--- client/src/pages/phonebook/phonebook.page.component.jsx | 4 ++-- client/src/pages/shop-csi/shop-csi.container.page.jsx | 5 ++--- 12 files changed, 23 insertions(+), 32 deletions(-) diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx index e98266464..bb265060f 100644 --- a/client/src/components/inventory-list/inventory-list.container.jsx +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -11,7 +11,6 @@ import { } from "../../redux/application/application.actions"; import AlertComponent from "../alert/alert.component"; import InventoryListPaginated from "./inventory-list.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -33,8 +32,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, consumedIsNull: showall === "true" ? null : true, order: [ { diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index b7bd3141d..885b53634 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -38,7 +38,7 @@ export function JobsList({bodyshop,}) { const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { variables: { offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + limit: 10, statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], order: [ { diff --git a/client/src/components/owners-list/owners-list.container.jsx b/client/src/components/owners-list/owners-list.container.jsx index 7303c4222..78db6bf2b 100644 --- a/client/src/components/owners-list/owners-list.container.jsx +++ b/client/src/components/owners-list/owners-list.container.jsx @@ -5,7 +5,6 @@ import AlertComponent from "../alert/alert.component"; import OwnersListComponent from "./owners-list.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; export default function OwnersListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -17,8 +16,8 @@ export default function OwnersListContainer() { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/components/vehicles-list/vehicles-list.container.jsx b/client/src/components/vehicles-list/vehicles-list.container.jsx index 2e85236f8..34fc4940b 100644 --- a/client/src/components/vehicles-list/vehicles-list.container.jsx +++ b/client/src/components/vehicles-list/vehicles-list.container.jsx @@ -5,7 +5,6 @@ import AlertComponent from "../alert/alert.component"; import { QUERY_ALL_VEHICLES_PAGINATED } from "../../graphql/vehicles.queries"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; export default function VehiclesListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -16,8 +15,8 @@ export default function VehiclesListContainer() { { variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx index 40e662899..22a8b61b9 100644 --- a/client/src/pages/bills/bills.page.container.jsx +++ b/client/src/pages/bills/bills.page.container.jsx @@ -13,7 +13,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import BillsPageComponent from "./bills.page.component"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -39,8 +38,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/contracts/contracts.page.container.jsx b/client/src/pages/contracts/contracts.page.container.jsx index 466b468e0..c4f4fef7b 100644 --- a/client/src/pages/contracts/contracts.page.container.jsx +++ b/client/src/pages/contracts/contracts.page.container.jsx @@ -12,7 +12,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import ContractsPageComponent from "./contracts.page.component"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -30,8 +29,8 @@ export function ContractsPageContainer({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx index 93bffe97f..e41e80f40 100644 --- a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx +++ b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx @@ -19,7 +19,6 @@ import NotFound from "../../components/not-found/not-found.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -42,8 +41,8 @@ export function CourtesyCarDetailPageContainer({ const { loading, error, data } = useQuery(QUERY_CC_BY_PK, { variables: { id: ccId, - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/export-logs/export-logs.page.component.jsx b/client/src/pages/export-logs/export-logs.page.component.jsx index 7ae8977cf..0c6517a58 100644 --- a/client/src/pages/export-logs/export-logs.page.component.jsx +++ b/client/src/pages/export-logs/export-logs.page.component.jsx @@ -30,8 +30,8 @@ export function ExportLogsPageComponent({ bodyshop }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index b9fa9a52a..dd78369a0 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -13,7 +13,6 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -34,8 +33,8 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 10 : 0, + limit: 25, ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ { diff --git a/client/src/pages/payments-all/payments-all.container.page.jsx b/client/src/pages/payments-all/payments-all.container.page.jsx index 3d3cb6287..07547bc2e 100644 --- a/client/src/pages/payments-all/payments-all.container.page.jsx +++ b/client/src/pages/payments-all/payments-all.container.page.jsx @@ -14,7 +14,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -35,8 +34,8 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/phonebook/phonebook.page.component.jsx b/client/src/pages/phonebook/phonebook.page.component.jsx index 5ce863444..baf4e18ec 100644 --- a/client/src/pages/phonebook/phonebook.page.component.jsx +++ b/client/src/pages/phonebook/phonebook.page.component.jsx @@ -36,8 +36,8 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "lastname"]: sortorder diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index f0c295685..132ee7999 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -16,7 +16,6 @@ import { } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -43,8 +42,8 @@ export function ShopCsiContainer({ nextFetchPolicy: "network-only", variables: { //search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "completedon"]: sortorder From 0852d55837f540892c9f1714380cd2b1d9731334 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 30 Nov 2023 00:22:20 +0000 Subject: [PATCH 002/131] Revert "Revert "Fix issues with limits. (pull request #1090)" --- .../components/inventory-list/inventory-list.container.jsx | 5 +++-- client/src/components/jobs-list/jobs-list.component.jsx | 2 +- client/src/components/owners-list/owners-list.container.jsx | 5 +++-- .../src/components/vehicles-list/vehicles-list.container.jsx | 5 +++-- client/src/pages/bills/bills.page.container.jsx | 5 +++-- client/src/pages/contracts/contracts.page.container.jsx | 5 +++-- .../courtesy-car-detail.page.container.jsx | 5 +++-- client/src/pages/export-logs/export-logs.page.component.jsx | 4 ++-- client/src/pages/jobs-all/jobs-all.container.jsx | 5 +++-- .../src/pages/payments-all/payments-all.container.page.jsx | 5 +++-- client/src/pages/phonebook/phonebook.page.component.jsx | 4 ++-- client/src/pages/shop-csi/shop-csi.container.page.jsx | 5 +++-- 12 files changed, 32 insertions(+), 23 deletions(-) diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx index bb265060f..e98266464 100644 --- a/client/src/components/inventory-list/inventory-list.container.jsx +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -11,6 +11,7 @@ import { } from "../../redux/application/application.actions"; import AlertComponent from "../alert/alert.component"; import InventoryListPaginated from "./inventory-list.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -32,8 +33,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, consumedIsNull: showall === "true" ? null : true, order: [ { diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 885b53634..b7bd3141d 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -38,7 +38,7 @@ export function JobsList({bodyshop,}) { const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { variables: { offset: page ? (page - 1) * pageLimit : 0, - limit: 10, + limit: pageLimit, statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], order: [ { diff --git a/client/src/components/owners-list/owners-list.container.jsx b/client/src/components/owners-list/owners-list.container.jsx index 78db6bf2b..7303c4222 100644 --- a/client/src/components/owners-list/owners-list.container.jsx +++ b/client/src/components/owners-list/owners-list.container.jsx @@ -5,6 +5,7 @@ import AlertComponent from "../alert/alert.component"; import OwnersListComponent from "./owners-list.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; export default function OwnersListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -16,8 +17,8 @@ export default function OwnersListContainer() { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/components/vehicles-list/vehicles-list.container.jsx b/client/src/components/vehicles-list/vehicles-list.container.jsx index 34fc4940b..2e85236f8 100644 --- a/client/src/components/vehicles-list/vehicles-list.container.jsx +++ b/client/src/components/vehicles-list/vehicles-list.container.jsx @@ -5,6 +5,7 @@ import AlertComponent from "../alert/alert.component"; import { QUERY_ALL_VEHICLES_PAGINATED } from "../../graphql/vehicles.queries"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; export default function VehiclesListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -15,8 +16,8 @@ export default function VehiclesListContainer() { { variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx index 22a8b61b9..40e662899 100644 --- a/client/src/pages/bills/bills.page.container.jsx +++ b/client/src/pages/bills/bills.page.container.jsx @@ -13,6 +13,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import BillsPageComponent from "./bills.page.component"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -38,8 +39,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/contracts/contracts.page.container.jsx b/client/src/pages/contracts/contracts.page.container.jsx index c4f4fef7b..466b468e0 100644 --- a/client/src/pages/contracts/contracts.page.container.jsx +++ b/client/src/pages/contracts/contracts.page.container.jsx @@ -12,6 +12,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import ContractsPageComponent from "./contracts.page.component"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -29,8 +30,8 @@ export function ContractsPageContainer({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx index e41e80f40..93bffe97f 100644 --- a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx +++ b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx @@ -19,6 +19,7 @@ import NotFound from "../../components/not-found/not-found.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -41,8 +42,8 @@ export function CourtesyCarDetailPageContainer({ const { loading, error, data } = useQuery(QUERY_CC_BY_PK, { variables: { id: ccId, - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/export-logs/export-logs.page.component.jsx b/client/src/pages/export-logs/export-logs.page.component.jsx index 0c6517a58..7ae8977cf 100644 --- a/client/src/pages/export-logs/export-logs.page.component.jsx +++ b/client/src/pages/export-logs/export-logs.page.component.jsx @@ -30,8 +30,8 @@ export function ExportLogsPageComponent({ bodyshop }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index dd78369a0..b9fa9a52a 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -13,6 +13,7 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -33,8 +34,8 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 10 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ { diff --git a/client/src/pages/payments-all/payments-all.container.page.jsx b/client/src/pages/payments-all/payments-all.container.page.jsx index 07547bc2e..3d3cb6287 100644 --- a/client/src/pages/payments-all/payments-all.container.page.jsx +++ b/client/src/pages/payments-all/payments-all.container.page.jsx @@ -14,6 +14,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -34,8 +35,8 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/phonebook/phonebook.page.component.jsx b/client/src/pages/phonebook/phonebook.page.component.jsx index baf4e18ec..5ce863444 100644 --- a/client/src/pages/phonebook/phonebook.page.component.jsx +++ b/client/src/pages/phonebook/phonebook.page.component.jsx @@ -36,8 +36,8 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "lastname"]: sortorder diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index 132ee7999..f0c295685 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -16,6 +16,7 @@ import { } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -42,8 +43,8 @@ export function ShopCsiContainer({ nextFetchPolicy: "network-only", variables: { //search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "completedon"]: sortorder From 3ca6308dd2cf6bbfe8d83b7c6b1f450fc1354f54 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 30 Nov 2023 20:22:42 +0000 Subject: [PATCH 003/131] Revert "Revert "Revert "Fix issues with limits. (pull request #1091)" --- .../components/inventory-list/inventory-list.container.jsx | 5 ++--- client/src/components/jobs-list/jobs-list.component.jsx | 2 +- client/src/components/owners-list/owners-list.container.jsx | 5 ++--- .../src/components/vehicles-list/vehicles-list.container.jsx | 5 ++--- client/src/pages/bills/bills.page.container.jsx | 5 ++--- client/src/pages/contracts/contracts.page.container.jsx | 5 ++--- .../courtesy-car-detail.page.container.jsx | 5 ++--- client/src/pages/export-logs/export-logs.page.component.jsx | 4 ++-- client/src/pages/jobs-all/jobs-all.container.jsx | 5 ++--- .../src/pages/payments-all/payments-all.container.page.jsx | 5 ++--- client/src/pages/phonebook/phonebook.page.component.jsx | 4 ++-- client/src/pages/shop-csi/shop-csi.container.page.jsx | 5 ++--- 12 files changed, 23 insertions(+), 32 deletions(-) diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx index e98266464..bb265060f 100644 --- a/client/src/components/inventory-list/inventory-list.container.jsx +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -11,7 +11,6 @@ import { } from "../../redux/application/application.actions"; import AlertComponent from "../alert/alert.component"; import InventoryListPaginated from "./inventory-list.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -33,8 +32,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, consumedIsNull: showall === "true" ? null : true, order: [ { diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index b7bd3141d..885b53634 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -38,7 +38,7 @@ export function JobsList({bodyshop,}) { const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { variables: { offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + limit: 10, statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], order: [ { diff --git a/client/src/components/owners-list/owners-list.container.jsx b/client/src/components/owners-list/owners-list.container.jsx index 7303c4222..78db6bf2b 100644 --- a/client/src/components/owners-list/owners-list.container.jsx +++ b/client/src/components/owners-list/owners-list.container.jsx @@ -5,7 +5,6 @@ import AlertComponent from "../alert/alert.component"; import OwnersListComponent from "./owners-list.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; export default function OwnersListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -17,8 +16,8 @@ export default function OwnersListContainer() { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/components/vehicles-list/vehicles-list.container.jsx b/client/src/components/vehicles-list/vehicles-list.container.jsx index 2e85236f8..34fc4940b 100644 --- a/client/src/components/vehicles-list/vehicles-list.container.jsx +++ b/client/src/components/vehicles-list/vehicles-list.container.jsx @@ -5,7 +5,6 @@ import AlertComponent from "../alert/alert.component"; import { QUERY_ALL_VEHICLES_PAGINATED } from "../../graphql/vehicles.queries"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; export default function VehiclesListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -16,8 +15,8 @@ export default function VehiclesListContainer() { { variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx index 40e662899..22a8b61b9 100644 --- a/client/src/pages/bills/bills.page.container.jsx +++ b/client/src/pages/bills/bills.page.container.jsx @@ -13,7 +13,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import BillsPageComponent from "./bills.page.component"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -39,8 +38,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/contracts/contracts.page.container.jsx b/client/src/pages/contracts/contracts.page.container.jsx index 466b468e0..c4f4fef7b 100644 --- a/client/src/pages/contracts/contracts.page.container.jsx +++ b/client/src/pages/contracts/contracts.page.container.jsx @@ -12,7 +12,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import ContractsPageComponent from "./contracts.page.component"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -30,8 +29,8 @@ export function ContractsPageContainer({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx index 93bffe97f..e41e80f40 100644 --- a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx +++ b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx @@ -19,7 +19,6 @@ import NotFound from "../../components/not-found/not-found.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -42,8 +41,8 @@ export function CourtesyCarDetailPageContainer({ const { loading, error, data } = useQuery(QUERY_CC_BY_PK, { variables: { id: ccId, - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/export-logs/export-logs.page.component.jsx b/client/src/pages/export-logs/export-logs.page.component.jsx index 7ae8977cf..0c6517a58 100644 --- a/client/src/pages/export-logs/export-logs.page.component.jsx +++ b/client/src/pages/export-logs/export-logs.page.component.jsx @@ -30,8 +30,8 @@ export function ExportLogsPageComponent({ bodyshop }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index b9fa9a52a..dd78369a0 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -13,7 +13,6 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -34,8 +33,8 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 10 : 0, + limit: 25, ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ { diff --git a/client/src/pages/payments-all/payments-all.container.page.jsx b/client/src/pages/payments-all/payments-all.container.page.jsx index 3d3cb6287..07547bc2e 100644 --- a/client/src/pages/payments-all/payments-all.container.page.jsx +++ b/client/src/pages/payments-all/payments-all.container.page.jsx @@ -14,7 +14,6 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -35,8 +34,8 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/phonebook/phonebook.page.component.jsx b/client/src/pages/phonebook/phonebook.page.component.jsx index 5ce863444..baf4e18ec 100644 --- a/client/src/pages/phonebook/phonebook.page.component.jsx +++ b/client/src/pages/phonebook/phonebook.page.component.jsx @@ -36,8 +36,8 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "lastname"]: sortorder diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index f0c295685..132ee7999 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -16,7 +16,6 @@ import { } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -43,8 +42,8 @@ export function ShopCsiContainer({ nextFetchPolicy: "network-only", variables: { //search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, + offset: page ? (page - 1) * 25 : 0, + limit: 25, order: [ { [sortcolumn || "completedon"]: sortorder From 5c95c72f408cdd3931a68c99f9c7bf53c3f204e4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 1 Dec 2023 02:37:10 +0000 Subject: [PATCH 004/131] Revert "Revert "Revert "Revert "Fix issues with limits. (pull request #1097)" --- .../components/inventory-list/inventory-list.container.jsx | 5 +++-- client/src/components/jobs-list/jobs-list.component.jsx | 2 +- client/src/components/owners-list/owners-list.container.jsx | 5 +++-- .../src/components/vehicles-list/vehicles-list.container.jsx | 5 +++-- client/src/pages/bills/bills.page.container.jsx | 5 +++-- client/src/pages/contracts/contracts.page.container.jsx | 5 +++-- .../courtesy-car-detail.page.container.jsx | 5 +++-- client/src/pages/export-logs/export-logs.page.component.jsx | 4 ++-- client/src/pages/jobs-all/jobs-all.container.jsx | 5 +++-- .../src/pages/payments-all/payments-all.container.page.jsx | 5 +++-- client/src/pages/phonebook/phonebook.page.component.jsx | 4 ++-- client/src/pages/shop-csi/shop-csi.container.page.jsx | 5 +++-- 12 files changed, 32 insertions(+), 23 deletions(-) diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx index bb265060f..e98266464 100644 --- a/client/src/components/inventory-list/inventory-list.container.jsx +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -11,6 +11,7 @@ import { } from "../../redux/application/application.actions"; import AlertComponent from "../alert/alert.component"; import InventoryListPaginated from "./inventory-list.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -32,8 +33,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, consumedIsNull: showall === "true" ? null : true, order: [ { diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 885b53634..b7bd3141d 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -38,7 +38,7 @@ export function JobsList({bodyshop,}) { const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { variables: { offset: page ? (page - 1) * pageLimit : 0, - limit: 10, + limit: pageLimit, statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], order: [ { diff --git a/client/src/components/owners-list/owners-list.container.jsx b/client/src/components/owners-list/owners-list.container.jsx index 78db6bf2b..7303c4222 100644 --- a/client/src/components/owners-list/owners-list.container.jsx +++ b/client/src/components/owners-list/owners-list.container.jsx @@ -5,6 +5,7 @@ import AlertComponent from "../alert/alert.component"; import OwnersListComponent from "./owners-list.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; export default function OwnersListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -16,8 +17,8 @@ export default function OwnersListContainer() { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/components/vehicles-list/vehicles-list.container.jsx b/client/src/components/vehicles-list/vehicles-list.container.jsx index 34fc4940b..2e85236f8 100644 --- a/client/src/components/vehicles-list/vehicles-list.container.jsx +++ b/client/src/components/vehicles-list/vehicles-list.container.jsx @@ -5,6 +5,7 @@ import AlertComponent from "../alert/alert.component"; import { QUERY_ALL_VEHICLES_PAGINATED } from "../../graphql/vehicles.queries"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; export default function VehiclesListContainer() { const searchParams = queryString.parse(useLocation().search); @@ -15,8 +16,8 @@ export default function VehiclesListContainer() { { variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx index 22a8b61b9..40e662899 100644 --- a/client/src/pages/bills/bills.page.container.jsx +++ b/client/src/pages/bills/bills.page.container.jsx @@ -13,6 +13,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import BillsPageComponent from "./bills.page.component"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -38,8 +39,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/contracts/contracts.page.container.jsx b/client/src/pages/contracts/contracts.page.container.jsx index c4f4fef7b..466b468e0 100644 --- a/client/src/pages/contracts/contracts.page.container.jsx +++ b/client/src/pages/contracts/contracts.page.container.jsx @@ -12,6 +12,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import ContractsPageComponent from "./contracts.page.component"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -29,8 +30,8 @@ export function ContractsPageContainer({ setBreadcrumbs, setSelectedHeader }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx index e41e80f40..93bffe97f 100644 --- a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx +++ b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx @@ -19,6 +19,7 @@ import NotFound from "../../components/not-found/not-found.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import queryString from "query-string"; import { useLocation } from "react-router-dom"; +import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -41,8 +42,8 @@ export function CourtesyCarDetailPageContainer({ const { loading, error, data } = useQuery(QUERY_CC_BY_PK, { variables: { id: ccId, - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "start"]: sortorder diff --git a/client/src/pages/export-logs/export-logs.page.component.jsx b/client/src/pages/export-logs/export-logs.page.component.jsx index 0c6517a58..7ae8977cf 100644 --- a/client/src/pages/export-logs/export-logs.page.component.jsx +++ b/client/src/pages/export-logs/export-logs.page.component.jsx @@ -30,8 +30,8 @@ export function ExportLogsPageComponent({ bodyshop }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "created_at"]: sortorder diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index dd78369a0..b9fa9a52a 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -13,6 +13,7 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //bodyshop: selectBodyshop, @@ -33,8 +34,8 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 10 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ { diff --git a/client/src/pages/payments-all/payments-all.container.page.jsx b/client/src/pages/payments-all/payments-all.container.page.jsx index 07547bc2e..3d3cb6287 100644 --- a/client/src/pages/payments-all/payments-all.container.page.jsx +++ b/client/src/pages/payments-all/payments-all.container.page.jsx @@ -14,6 +14,7 @@ import { setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -34,8 +35,8 @@ export function AllJobs({ bodyshop, setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ searchObj ? JSON.parse(searchObj) diff --git a/client/src/pages/phonebook/phonebook.page.component.jsx b/client/src/pages/phonebook/phonebook.page.component.jsx index baf4e18ec..5ce863444 100644 --- a/client/src/pages/phonebook/phonebook.page.component.jsx +++ b/client/src/pages/phonebook/phonebook.page.component.jsx @@ -36,8 +36,8 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) { nextFetchPolicy: "network-only", variables: { search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "lastname"]: sortorder diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index 132ee7999..f0c295685 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -16,6 +16,7 @@ import { } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -42,8 +43,8 @@ export function ShopCsiContainer({ nextFetchPolicy: "network-only", variables: { //search: search || "", - offset: page ? (page - 1) * 25 : 0, - limit: 25, + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, order: [ { [sortcolumn || "completedon"]: sortorder From 02b6875eecd5ed98e61a88228cfea9a37bcd942e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 15:41:10 -0800 Subject: [PATCH 005/131] IO-2602 Beta domain --- server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server.js b/server.js index a64545b72..5eebba671 100644 --- a/server.js +++ b/server.js @@ -34,6 +34,10 @@ const io = new Server(server, { "http://localhost:3000", "https://imex.online", "https://www.imex.online", + "https://beta.test.imex.online", + "https://www.beta.test.imex.online", + "https://beta.imex.online", + "https://www.beta.imex.online", ], methods: ["GET", "POST"], credentials: true, From 4c4e16b0c902cf0ba55f9bfd060c0cd48ffc35d0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 12 Jan 2024 20:47:06 -0500 Subject: [PATCH 006/131] - Add in the Beta Switch on test Signed-off-by: Dave Richer --- client/src/App/App.jsx | 7 ++++ .../components/header/header.component.jsx | 30 +++++++++++++-- client/src/utils/handleBeta.js | 37 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 client/src/utils/handleBeta.js diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 618366db4..7d396eef8 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -22,6 +22,7 @@ import { } from "../redux/user/user.selectors"; import PrivateRoute from "../utils/private-route"; import "./App.styles.scss"; +import handleBeta from "../utils/handleBeta"; const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component") @@ -53,6 +54,12 @@ export function App({ }) { const client = useClient(); + // Handle The Beta Switch. + useEffect(() => { + handleBeta(); + }, []) + + useEffect(() => { if (!navigator.onLine) { setOnline(false); diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 8d180bdd1..404792639 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -13,7 +13,7 @@ import Icon, { FileFilled, //GlobalOutlined, HomeFilled, - ImportOutlined, + ImportOutlined, InfoCircleOutlined, LineChartOutlined, PaperClipOutlined, PhoneOutlined, @@ -26,8 +26,8 @@ import Icon, { UserOutlined, } from "@ant-design/icons"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Layout, Menu } from "antd"; -import React from "react"; +import {Layout, Menu, Switch, Tooltip} from "antd"; +import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; import { @@ -52,6 +52,7 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -102,9 +103,21 @@ function Header({ {}, bodyshop && bodyshop.imexshopid ); + const [betaSwitch, setBetaSwitch] = useState(false); const { t } = useTranslation(); + useEffect(() => { + const isBeta = checkBeta(); + setBetaSwitch(isBeta); + }, []); + + const betaSwitchChange = (checked) => { + setBeta(checked); + setBetaSwitch(checked); + handleBeta(); + } + return ( ))} + + + + Try the new ImEX Online + + + + ); diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js new file mode 100644 index 000000000..155c09f9e --- /dev/null +++ b/client/src/utils/handleBeta.js @@ -0,0 +1,37 @@ +import React, {useState} from "react"; + +export const BETA_KEY = 'betaSwitchImex'; + +export const checkBeta = () => { + const cookie = document.cookie.split('; ').find(row => row.startsWith(BETA_KEY)); + return cookie ? cookie.split('=')[1] === 'true' : false; +} + + +export const setBeta = (value) => { + const domain = window.location.hostname.split('.').slice(-2).join('.'); + document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`; +} + +export const handleBeta = () => { + // If the current host name does not start with beta or test, then we don't need to do anything. + if (window.location.hostname.startsWith('localhost')) { + console.log('Not on beta or test, so no need to handle beta.'); + return; + } + + const isBeta = checkBeta(); + + const currentHostName = window.location.hostname; + + // Beta is enabled, but the current host name does start with beta. + if (isBeta && !currentHostName.startsWith('beta')) { + window.location.href = `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; + } + + // Beta is not enabled, but the current host name does start with beta. + else if (!isBeta && currentHostName.startsWith('beta')) { + window.location.href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; + } +} +export default handleBeta; From 3704c0cb12a23b4f3fd2b201b3f24f7b1fb492a4 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 12 Jan 2024 20:50:40 -0500 Subject: [PATCH 007/131] - Add in the Beta Switch on test Signed-off-by: Dave Richer --- client/src/utils/handleBeta.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js index 155c09f9e..428d4fb49 100644 --- a/client/src/utils/handleBeta.js +++ b/client/src/utils/handleBeta.js @@ -1,5 +1,3 @@ -import React, {useState} from "react"; - export const BETA_KEY = 'betaSwitchImex'; export const checkBeta = () => { From 7245d4eab24cdb682c4fa9bca639eabff9e8be02 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 15 Jan 2024 18:54:28 -0800 Subject: [PATCH 008/131] IO-2603 Open Orders Excel --- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/TemplateConstants.js | 13 +++++++++++++ 4 files changed, 16 insertions(+) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 09798f0a5..46dfbe01f 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2621,6 +2621,7 @@ "open_orders": "Open Orders by Date", "open_orders_csr": "Open Orders by CSR", "open_orders_estimator": "Open Orders by Estimator", + "open_orders_excel": "Open Orders - Excel", "open_orders_ins_co": "Open Orders by Insurance Company", "open_orders_referral": "Open Orders by Referral Source", "open_orders_specific_csr": "Open Orders filtered by CSR", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index da9f6b4e0..e7688ca48 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2621,6 +2621,7 @@ "open_orders": "", "open_orders_csr": "", "open_orders_estimator": "", + "open_orders_excel": "", "open_orders_ins_co": "", "open_orders_referral": "", "open_orders_specific_csr": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a4c1dc686..4121c0c21 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2621,6 +2621,7 @@ "open_orders": "", "open_orders_csr": "", "open_orders_estimator": "", + "open_orders_excel": "", "open_orders_ins_co": "", "open_orders_referral": "", "open_orders_specific_csr": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 365b2431e..eeb937c3a 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2026,6 +2026,19 @@ export const TemplateList = (type, context) => { }, group: "customers", }, + open_orders_excel: { + title: i18n.t("reportcenter.templates.open_orders_excel"), + subject: i18n.t("reportcenter.templates.open_orders_excel"), + key: "open_orders_excel", + //idtype: "vendor", + reporttype: "excel", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + group: "jobs", + }, } : {}), ...(!type || type === "courtesycarcontract" From 2ce85495021b9a806cbad18883c1a8998e9091c8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 16 Jan 2024 12:11:18 -0800 Subject: [PATCH 009/131] IO-2531 Retain Filtered Statue for Jobs Pages --- .../jobs-list-paginated.component.jsx | 7 +- .../jobs-list/jobs-list.component.jsx | 725 +++++++++--------- .../jobs-ready-list.component.jsx | 44 +- 3 files changed, 397 insertions(+), 379 deletions(-) diff --git a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx index b5048d0ef..1b4c6f435 100644 --- a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx +++ b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx @@ -10,9 +10,10 @@ import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { pageLimit } from "../../utils/config"; +import useLocalStorage from "../../utils/useLocalStorage"; import StartChatButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, @@ -25,6 +26,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { const search = queryString.parse(useLocation().search); const [openSearchResults, setOpenSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); + const [filter, setFilter] = useLocalStorage("filter_jobs_all", null); + console.log("filter", filter); const { page, sortcolumn, sortorder } = search; const history = useHistory(); @@ -93,6 +96,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { render: (text, record) => { return record.status || t("general.labels.na"); }, + filteredValue: filter?.status || null, filters: bodyshop.md_ro_statuses.statuses.map((s) => { return { text: s, value: [s] }; }), @@ -189,6 +193,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { } else { delete search.statusFilters; } + setFilter(filters); history.push({ search: queryString.stringify(search) }); }; diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index fb2e1daa6..7761c0ed9 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -1,8 +1,8 @@ import { - SyncOutlined, + BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, - BranchesOutlined, + SyncOutlined, } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd"; @@ -14,382 +14,389 @@ import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import { onlyUnique } from "../../utils/arrayHelper"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { alphaSort } from "../../utils/sorters"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { alphaSort, statusSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop, }); export function JobsList({ bodyshop }) { - const searchParams = queryString.parse(useLocation().search); - const { selected } = searchParams; - const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) - .filter((screen) => !!screen[1]) - .slice(-1)[0]; - const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { - variables: { - statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const searchParams = queryString.parse(useLocation().search); + const { selected } = searchParams; + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; + const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { + variables: { + statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], + }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: { text: "" }, - }); + const [state, setState] = useState({ sortedInfo: {} }); + const [filter, setFilter] = useLocalStorage("filter_jobs_list", null); - const { t } = useTranslation(); - const history = useHistory(); - const [searchText, setSearchText] = useState(""); + const { t } = useTranslation(); + const history = useHistory(); + const [searchText, setSearchText] = useState(""); - if (error) return ; + if (error) return ; - const jobs = data - ? searchText === "" - ? data.jobs - : data.jobs.filter( - (j) => - (j.ro_number || "") - .toString() - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_co_nm || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.comments || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_fn || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_ln || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || - (j.plate_no || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.v_model_desc || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.est_ct_fn || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.est_ct_ln || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.v_make_desc || "") - .toLowerCase() - .includes(searchText.toLowerCase()) - ) - : []; + const jobs = data + ? searchText === "" + ? data.jobs + : data.jobs.filter( + (j) => + (j.ro_number || "") + .toString() + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_co_nm || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.comments || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || + (j.plate_no || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_model_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_make_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) + ) + : []; - const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); + }; - const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - history.push({ - search: queryString.stringify({ - ...searchParams, - selected: record.id, - }), - }); - } - } - }; + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + history.push({ + search: queryString.stringify({ + ...searchParams, + selected: record.id, + }), + }); + } + } + }; - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => - parseInt((a.ro_number || "0").replace(/\D/g, "")) - - parseInt((b.ro_number || "0").replace(/\D/g, "")), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - - render: (text, record) => ( - e.stopPropagation()} - > - - {record.ro_number || t("general.labels.na")} - {record.production_vars && record.production_vars.alert ? ( - - ) : null} - {record.suspended && ( - - )} - {record.iouparent && ( - - - - )} - - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - - responsive: ["md"], - sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - return record.ownerid ? ( - e.stopPropagation()} - > - - - ) : ( - + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => + parseInt((a.ro_number || "0").replace(/\D/g, "")) - + parseInt((b.ro_number || "0").replace(/\D/g, "")), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()} + > + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + responsive: ["md"], + sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + ) : ( + - ); + ); + }, + }, + { + title: t("jobs.fields.ownr_ph1"), + dataIndex: "ownr_ph1", + key: "ownr_ph1", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + { + title: t("jobs.fields.ownr_ph2"), + dataIndex: "ownr_ph2", + key: "ownr_ph2", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: filter?.status || null, + filters: + (jobs && + jobs + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + }) + .sort((a, b) => + statusSort( + a.text, + b.text, + bodyshop.md_ro_statuses.active_statuses + ) + )) || + [], + onFilter: (value, record) => value.includes(record.status), + }, + + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("vehicles.fields.plate_no"), + dataIndex: "plate_no", + key: "plate_no", + ellipsis: true, + + responsive: ["md"], + sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), + sortOrder: + state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + ellipsis: true, + responsive: ["md"], + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), + sortOrder: + state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, + render: (text, record) => + `${record.clm_no || ""}${ + record.po_number ? ` (PO: ${record.po_number})` : "" + }`, + }, + { + title: t("jobs.fields.ins_co_nm"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + filteredValue: filter?.ins_co_nm || null, + filters: + (jobs && + jobs + .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), + responsive: ["md"], + }, + { + title: t("jobs.fields.clm_total"), + dataIndex: "clm_total", + key: "clm_total", + responsive: ["md"], + ellipsis: true, + sorter: (a, b) => a.clm_total - b.clm_total, + sortOrder: + state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, + render: (text, record) => ( + {record.clm_total} + ), + }, + { + title: t("jobs.labels.estimator"), + dataIndex: "jobs.labels.estimator", + key: "estimator", + ellipsis: true, + responsive: ["xl"], + filterSearch: true, + filteredValue: filter?.estimator || null, + filters: + (jobs && + jobs + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Estimator*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + responsive: ["md"], + }, + // { + // title: t("jobs.fields.owner_owing"), + // dataIndex: "owner_owing", + // key: "owner_owing", + // responsive: ["md"], + // render: (text, record) => ( + // {record.owner_owing} + // ), + // }, + ]; + + const scrollMapper = { + xs: true, + sm: true, + md: true, + lg: "100%", + xl: "100%", + xxl: "100%", + }; + + return ( + + + { + setSearchText(e.target.value); + }} + value={searchText} + enterButton + /> + + } + > + { + handleOnRowClick(record); + }, + selectedRowKeys: [selected], + type: "radio", + }} + onChange={handleTableChange} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); }, - }, - { - title: t("jobs.fields.ownr_ph1"), - dataIndex: "ownr_ph1", - key: "ownr_ph1", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - ellipsis: true, - - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - filters: - (jobs && - jobs - .map((j) => j.status) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Status*", - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.status), - }, - - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - e.stopPropagation()} - > - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("vehicles.fields.plate_no"), - dataIndex: "plate_no", - key: "plate_no", - ellipsis: true, - - responsive: ["md"], - sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), - sortOrder: - state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, - }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - responsive: ["md"], - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: - state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, - render: (text, record) => - `${record.clm_no || ""}${ - record.po_number ? ` (PO: ${record.po_number})` : "" - }`, - }, - { - title: t("jobs.fields.ins_co_nm"), - dataIndex: "ins_co_nm", - key: "ins_co_nm", - ellipsis: true, - filters: - (jobs && - jobs - .map((j) => j.ins_co_nm) - .filter(onlyUnique) - .map((s) => { - return { - text: s, - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.ins_co_nm), - responsive: ["md"], - }, - { - title: t("jobs.fields.clm_total"), - dataIndex: "clm_total", - key: "clm_total", - responsive: ["md"], - ellipsis: true, - - sorter: (a, b) => a.clm_total - b.clm_total, - sortOrder: - state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, - render: (text, record) => ( - {record.clm_total} - ), - }, - { - title: t("jobs.labels.estimator"), - dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", - ellipsis: true, - responsive: ["xl"], - filterSearch: true, - filters: - (jobs && - jobs - .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "N/A", - value: [s], - }; - })) || - [], - onFilter: (value, record) => - value.includes( - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() - ), - render: (text, record) => - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), - }, - { - title: t("jobs.fields.comment"), - dataIndex: "comment", - key: "comment", - ellipsis: true, - responsive: ["md"], - }, - // { - // title: t("jobs.fields.owner_owing"), - // dataIndex: "owner_owing", - // key: "owner_owing", - // responsive: ["md"], - // render: (text, record) => ( - // {record.owner_owing} - // ), - // }, - ]; - - const scrollMapper = { - xs: true, - sm: true, - md: true, - lg: "100%", - xl: "100%", - xxl: "100%", - }; - - return ( - - - { - setSearchText(e.target.value); - }} - value={searchText} - enterButton - /> - - } - > -
{ - handleOnRowClick(record); - }, - selectedRowKeys: [selected], - type: "radio", - }} - onChange={handleTableChange} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); - }, - }; - }} - /> - - ); + }; + }} + /> + + ); } export default connect(mapStateToProps, null)(JobsList); diff --git a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx index fee461df2..147f6ea23 100644 --- a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx +++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx @@ -16,11 +16,12 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { onlyUnique } from "../../utils/arrayHelper"; -import { alphaSort } from "../../utils/sorters"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, statusSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -53,10 +54,8 @@ export function JobsReadyList({ bodyshop }) { nextFetchPolicy: "network-only", }); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: { text: "" }, - }); + const [state, setState] = useState({ sortedInfo: {} }); + const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null); const { t } = useTranslation(); const history = useHistory(); @@ -105,7 +104,8 @@ export function JobsReadyList({ bodyshop }) { : []; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); }; const handleOnRowClick = (record) => { @@ -129,7 +129,6 @@ export function JobsReadyList({ bodyshop }) { sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( alphaSort(a.ownr_ln, b.ownr_ln), sortOrder: @@ -197,16 +195,15 @@ export function JobsReadyList({ bodyshop }) { ), }, - { title: t("jobs.fields.status"), dataIndex: "status", key: "status", ellipsis: true, - sorter: (a, b) => alphaSort(a.status, b.status), sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: filter?.status || null, filters: (jobs && jobs @@ -217,11 +214,17 @@ export function JobsReadyList({ bodyshop }) { text: s || "No Status*", value: [s], }; - })) || + }) + .sort((a, b) => + statusSort( + a.text, + b.text, + bodyshop.md_ro_statuses.active_statuses + ) + )) || [], onFilter: (value, record) => value.includes(record.status), }, - { title: t("jobs.fields.vehicle"), dataIndex: "vehicle", @@ -274,6 +277,7 @@ export function JobsReadyList({ bodyshop }) { dataIndex: "ins_co_nm", key: "ins_co_nm", ellipsis: true, + filteredValue: filter?.ins_co_nm || null, filters: (jobs && jobs @@ -281,10 +285,11 @@ export function JobsReadyList({ bodyshop }) { .filter(onlyUnique) .map((s) => { return { - text: s, + text: s || "No Ins Co.*", value: [s], }; - })) || + }) + .sort((a, b) => alphaSort(a.text, b.text))) || [], onFilter: (value, record) => value.includes(record.ins_co_nm), responsive: ["md"], @@ -295,7 +300,6 @@ export function JobsReadyList({ bodyshop }) { key: "clm_total", responsive: ["md"], ellipsis: true, - sorter: (a, b) => a.clm_total - b.clm_total, sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, @@ -306,9 +310,10 @@ export function JobsReadyList({ bodyshop }) { { title: t("jobs.labels.estimator"), dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", + key: "estimator", ellipsis: true, responsive: ["xl"], + filteredValue: filter?.estimator || null, filterSearch: true, filters: (jobs && @@ -317,10 +322,11 @@ export function JobsReadyList({ bodyshop }) { .filter(onlyUnique) .map((s) => { return { - text: s || "N/A", + text: s || "No Estimator*", value: [s], }; - })) || + }) + .sort((a, b) => alphaSort(a.text, b.text))) || [], onFilter: (value, record) => value.includes( From 0cc367b25e9c78550ba65e7a19595b9f43a915db Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 16 Jan 2024 12:12:53 -0800 Subject: [PATCH 010/131] IO-2531 Remove Console Log --- .../jobs-list-paginated/jobs-list-paginated.component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx index 1b4c6f435..e247f5450 100644 --- a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx +++ b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx @@ -27,7 +27,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { const [openSearchResults, setOpenSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [filter, setFilter] = useLocalStorage("filter_jobs_all", null); - console.log("filter", filter); const { page, sortcolumn, sortorder } = search; const history = useHistory(); From dea7fd71efb5cc3d8952509f8b55386de9174eeb Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Jan 2024 09:41:57 -0800 Subject: [PATCH 011/131] IO-2600 Tech Console Job Logout Console Log --- .../tech-job-clock-out-button.component.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx index d1c5c3528..98369c860 100644 --- a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx +++ b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx @@ -166,9 +166,6 @@ export function TechClockOffButton({ }, ({ getFieldValue }) => ({ validator(rule, value) { - console.log( - bodyshop.tt_enforce_hours_for_tech_console - ); if (!bodyshop.tt_enforce_hours_for_tech_console) { return Promise.resolve(); } From 636be8989e952f351b2195d06a6bbc70993937b6 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Jan 2024 14:22:47 -0800 Subject: [PATCH 012/131] IO-2589 Allow Company Only for Customer Creation --- .../jobs-create-owner-info.new.component.jsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx index 2dea9a657..da2a1b1a5 100644 --- a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx +++ b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx @@ -19,10 +19,13 @@ export default function JobsCreateOwnerInfoNewComponent() { label={t("owners.fields.ownr_ln")} name={["owner", "data", "ownr_ln"]} rules={[ - { - required: state.owner.new, + ({ getFieldValue }) => ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_co_nm"]) || + getFieldValue(["owner", "data", "ownr_co_nm"]) === ""), //message: t("general.validation.required"), - }, + }), ]} > @@ -31,10 +34,13 @@ export default function JobsCreateOwnerInfoNewComponent() { label={t("owners.fields.ownr_fn")} name={["owner", "data", "ownr_fn"]} rules={[ - { - required: state.owner.new, + ({ getFieldValue }) => ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_co_nm"]) || + getFieldValue(["owner", "data", "ownr_co_nm"]) === ""), //message: t("general.validation.required"), - }, + }), ]} > @@ -51,6 +57,17 @@ export default function JobsCreateOwnerInfoNewComponent() { ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_ln"]) || + !getFieldValue(["owner", "data", "ownr_fn"]) || + getFieldValue(["owner", "data", "ownr_ln"]) === "" || + getFieldValue(["owner", "data", "ownr_fn"]) === ""), + //message: t("general.validation.required"), + }), + ]} > From 572963d9871dfb593f497ce1b9fd9659f6dd37e4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Jan 2024 17:09:17 -0800 Subject: [PATCH 013/131] IO-2606 Modal Closeable for Schedule Appointment --- .../schedule-job-modal/schedule-job-modal.container.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx index 19c360a21..19d6d4a37 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx @@ -216,6 +216,7 @@ export function ScheduleJobModalContainer({ okButtonProps={{ loading: loading, }} + closable={false} >
Date: Wed, 17 Jan 2024 17:52:14 -0800 Subject: [PATCH 014/131] IO-2601 Tech Console Titles --- .../src/components/tech-login/tech-login.component.jsx | 8 ++++++-- .../pages/tech-job-clock/tech-job-clock.component.jsx | 9 ++++++++- client/src/pages/tech-lookup/tech-lookup.container.jsx | 9 ++++++++- .../tech-shift-clock/tech-shift-clock.component.jsx | 9 ++++++++- client/src/translations/en_us/common.json | 8 ++++++-- client/src/translations/es/common.json | 6 +++++- client/src/translations/fr/common.json | 6 +++++- 7 files changed, 46 insertions(+), 9 deletions(-) diff --git a/client/src/components/tech-login/tech-login.component.jsx b/client/src/components/tech-login/tech-login.component.jsx index 502b20cbf..c266803a5 100644 --- a/client/src/components/tech-login/tech-login.component.jsx +++ b/client/src/components/tech-login/tech-login.component.jsx @@ -1,7 +1,8 @@ import { Button, Form, Input } from "antd"; -import React from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import { Redirect } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { techLoginStart } from "../../redux/tech/tech.actions"; import { @@ -11,7 +12,6 @@ import { } from "../../redux/tech/tech.selectors"; import AlertComponent from "../alert/alert.component"; import "./tech-login.styles.scss"; -import { Redirect } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ technician: selectTechnician, @@ -35,6 +35,10 @@ export function TechLogin({ techLoginStart(values); }; + useEffect(() => { + document.title = t("titles.techconsole"); + }, [t]); + return (
{technician ? : null} diff --git a/client/src/pages/tech-job-clock/tech-job-clock.component.jsx b/client/src/pages/tech-job-clock/tech-job-clock.component.jsx index 0d64068eb..c22b68eed 100644 --- a/client/src/pages/tech-job-clock/tech-job-clock.component.jsx +++ b/client/src/pages/tech-job-clock/tech-job-clock.component.jsx @@ -1,10 +1,17 @@ import { Divider } from "antd"; -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import TechClockInFormContainer from "../../components/tech-job-clock-in-form/tech-job-clock-in-form.container"; import TechClockedInList from "../../components/tech-job-clocked-in-list/tech-job-clocked-in-list.component"; import TechJobStatistics from "../../components/tech-job-statistics/tech-job-statistics.component"; export default function TechClockComponent() { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.techjobclock"); + }, [t]); + return (
diff --git a/client/src/pages/tech-lookup/tech-lookup.container.jsx b/client/src/pages/tech-lookup/tech-lookup.container.jsx index 1297220b9..e24f8f661 100644 --- a/client/src/pages/tech-lookup/tech-lookup.container.jsx +++ b/client/src/pages/tech-lookup/tech-lookup.container.jsx @@ -1,9 +1,16 @@ -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component"; import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component"; import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component"; export default function TechLookupContainer() { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.techjoblookup"); + }, [t]); + return (
diff --git a/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx b/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx index 52e0e99c7..2dad65e64 100644 --- a/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx +++ b/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx @@ -1,7 +1,14 @@ -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import TimeTicketShift from "../../components/time-ticket-shift/time-ticket-shift.container"; export default function TechShiftClock() { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.techshiftclock"); + }, [t]); + return (
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 09798f0a5..13555e364 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2024,7 +2024,7 @@ "joblookup": "Job Lookup", "login": "Login", "logout": "Logout", - "productionboard": "Production Board - Visual", + "productionboard": "Production Visual", "productionlist": "Production List", "shiftclockin": "Shift Clock" } @@ -2901,7 +2901,7 @@ "parts-queue": "Parts Queue | $t(titles.app)", "payments-all": "Payments | $t(titles.app)", "phonebook": "Phonebook | $t(titles.app)", - "productionboard": "Production - Board", + "productionboard": "Production Board - Visual | $t(titles.app)", "productionlist": "Production Board - List | $t(titles.app)", "profile": "My Profile | $t(titles.app)", "readyjobs": "Ready Jobs | $t(titles.app)", @@ -2913,6 +2913,10 @@ "shop-csi": "CSI Responses | $t(titles.app)", "shop-templates": "Shop Templates | $t(titles.app)", "shop_vendors": "Vendors | $t(titles.app)", + "techconsole": "Technician Console | $t(titles.app)", + "techjoblookup": "Technician Job Lookup | $t(titles.app)", + "techjobclock": "Technician Job Clock | $t(titles.app)", + "techshiftclock": "Technician Shift Clock | $t(titles.app)", "temporarydocs": "Temporary Documents | $t(titles.app)", "timetickets": "Time Tickets | $t(titles.app)", "ttapprovals": "", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index da9f6b4e0..9f522ecce 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2894,7 +2894,7 @@ "jobs-intake": "", "jobsavailable": "Empleos disponibles | $t(titles.app)", "jobsdetail": "Trabajo {{ro_number}} | $t(titles.app)", - "jobsdocuments": "Documentos de trabajo {{ro_number}} | $ t (títulos.app)", + "jobsdocuments": "Documentos de trabajo {{ro_number}} | $t(titles.app)", "manageroot": "Casa | $t(titles.app)", "owners": "Todos los propietarios | $t(titles.app)", "owners-detail": "", @@ -2913,6 +2913,10 @@ "shop-csi": "", "shop-templates": "", "shop_vendors": "Vendedores | $t(titles.app)", + "techconsole": "$t(titles.app)", + "techjoblookup": "$t(titles.app)", + "techjobclock": "$t(titles.app)", + "techshiftclock": "$t(titles.app)", "temporarydocs": "", "timetickets": "", "ttapprovals": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a4c1dc686..2543ae722 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2894,7 +2894,7 @@ "jobs-intake": "", "jobsavailable": "Emplois disponibles | $t(titles.app)", "jobsdetail": "Travail {{ro_number}} | $t(titles.app)", - "jobsdocuments": "Documents de travail {{ro_number}} | $ t (titres.app)", + "jobsdocuments": "Documents de travail {{ro_number}} | $t(titles.app)", "manageroot": "Accueil | $t(titles.app)", "owners": "Tous les propriétaires | $t(titles.app)", "owners-detail": "", @@ -2913,6 +2913,10 @@ "shop-csi": "", "shop-templates": "", "shop_vendors": "Vendeurs | $t(titles.app)", + "techconsole": "$t(titles.app)", + "techjoblookup": "$t(titles.app)", + "techjobclock": "$t(titles.app)", + "techshiftclock": "$t(titles.app)", "temporarydocs": "", "timetickets": "", "ttapprovals": "", From 430823dde0158387fcdbca1c26743919db67c0e1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 18 Jan 2024 13:20:04 -0500 Subject: [PATCH 015/131] - remove source maps from prod Signed-off-by: Dave Richer --- client/src/components/header/header.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 404792639..d22e37d89 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -445,7 +445,7 @@ function Header({ ))} - + Try the new ImEX Online Date: Thu, 18 Jan 2024 11:30:58 -0800 Subject: [PATCH 016/131] IO-2599 Tech Station Linking --- .../job-detail-cards.template.component.jsx | 28 +++- .../jobs-detail-header.component.jsx | 67 ++++++--- .../jobs-related-ros.component.jsx | 14 +- .../production-list-columns.data.js | 28 ++-- .../production-list-detail.component.jsx | 134 ++++++++++-------- client/src/graphql/jobs.queries.js | 7 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 9 files changed, 187 insertions(+), 94 deletions(-) diff --git a/client/src/components/job-detail-cards/job-detail-cards.template.component.jsx b/client/src/components/job-detail-cards/job-detail-cards.template.component.jsx index 4aa6e0d6e..0fa8d3c20 100644 --- a/client/src/components/job-detail-cards/job-detail-cards.template.component.jsx +++ b/client/src/components/job-detail-cards/job-detail-cards.template.component.jsx @@ -1,15 +1,37 @@ -import React from "react"; import { Card } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; -export default function JobDetailCardTemplate({ +const mapStateToProps = createStructuredSelector({ + technician: selectTechnician, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobDetailCardTemplate); + +export function JobDetailCardTemplate({ loading, title, extraLink, + technician, ...otherProps }) { + const { t } = useTranslation(); + let extra; - if (extraLink) extra = { extra: More }; + if (extraLink && !technician) + extra = { + extra: {t("jobs.labels.cards.more")}, + }; return ( - {ownerTitle.length > 0 - ? ownerTitle - : t("owner.labels.noownerinfo")} - + disabled ? ( + <> + {ownerTitle.length > 0 + ? ownerTitle + : t("owner.labels.noownerinfo")} + + ) : ( + + {ownerTitle.length > 0 + ? ownerTitle + : t("owner.labels.noownerinfo")} + + ) } >
- + {disabled ? ( + {job.ownr_ph1} + ) : ( + + )} - + {disabled ? ( + {job.ownr_ph2} + ) : ( + + )} {`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${ @@ -180,7 +197,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { } ${job.ownr_st || ""} ${job.ownr_zip || ""}`} - {job.ownr_ea || ""} + {disabled ? ( + <>{job.ownr_ea || ""} + ) : job.ownr_ea ? ( + {job.ownr_ea} + ) : null} {job.owner?.tax_number && ( @@ -195,17 +216,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { style={{ height: "100%" }} title={ job.vehicle ? ( - - {vehicleTitle.length > 0 - ? vehicleTitle - : t("vehicles.labels.novehinfo")} - + disabled ? ( + <> + {vehicleTitle.length > 0 + ? vehicleTitle + : t("vehicles.labels.novehinfo")}{" "} + + ) : ( + + {vehicleTitle.length > 0 + ? vehicleTitle + : t("vehicles.labels.novehinfo")} + + ) ) : ( ) @@ -223,7 +246,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( job.v_vin?.length !== 17 ? ( - + ) : null ) : null} @@ -231,7 +256,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {job.regie_number || t("general.labels.na")} - + {job.vehicle && job.vehicle.notes && ( @@ -10,9 +10,15 @@ export default function JobsRelatedRos({ jobid, job }) { .filter((j) => j.id !== job.id) .map((j) => ( - {`${j.ro_number || "N/A"}${ - j.clm_no ? ` | ${j.clm_no}` : "" - }${j.status ? ` | ${j.status}` : ""}`} + {disabled ? ( + <>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${ + j.status ? ` | ${j.status}` : "" + }`} + ) : ( + {`${j.ro_number || "N/A"}${ + j.clm_no ? ` | ${j.clm_no}` : "" + }${j.status ? ` | ${j.status}` : ""}`} + )} ))} diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js index 8e584aa2e..271ed2f15 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.js +++ b/client/src/components/production-list-columns/production-list-columns.data.js @@ -76,7 +76,14 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => { dataIndex: "ownr", key: "ownr", ellipsis: true, - render: (text, record) => , + render: (text, record) => + technician ? ( + + ) : ( + + + + ), sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sortOrder: state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, @@ -93,13 +100,18 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => { ), sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, - render: (text, record) => ( - {`${ - record.v_model_yr || "" - } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ - record.v_color || "" - } ${record.plate_no || ""}`} - ), + render: (text, record) => + technician ? ( + <>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + } ${record.v_color || ""} ${record.plate_no || ""}`} + ) : ( + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ + record.v_color || "" + } ${record.plate_no || ""}`} + ), }, { title: i18n.t("jobs.fields.actual_in"), diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx index 170b31b46..2963176b9 100644 --- a/client/src/components/production-list-detail/production-list-detail.component.jsx +++ b/client/src/components/production-list-detail/production-list-detail.component.jsx @@ -13,12 +13,14 @@ import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter } from "../../utils/DateFormatter"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import AlertComponent from "../alert/alert.component"; import StartChatButton from "../chat-open-button/chat-open-button.component"; import JobAtChange from "../job-at-change/job-at-change.component"; import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component"; import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component"; import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component"; +import CardTemplate from "../job-detail-cards/job-detail-cards.template.component"; import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container"; import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; @@ -103,65 +105,85 @@ export function ProductionListDetail({ {error && } {!loading && data && (
- - - - {theJob.ro_number || ""} - - - - {data.jobs_by_pk.alt_transport || ""} - - - - - {theJob.clm_no || ""} - - - {theJob.ins_co_nm || ""} - - - - - - - - {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ - theJob.v_make_desc || "" - } ${theJob.v_model_desc || ""}`} - - - {theJob.clm_total} - - - {theJob.actual_in} - - - {theJob.scheduled_completion} - - - - - - - {!bodyshop.uselocalmediaserver && ( - + + + + + + {theJob.ro_number || ""} + + + + {data.jobs_by_pk.alt_transport || ""} + + + + + {theJob.clm_no || ""} + + + {theJob.ins_co_nm || ""} + + + + + {!technician ? ( + <> + + + + ) : ( + <> + + {data.jobs_by_pk.ownr_ph1} + + + {data.jobs_by_pk.ownr_ph2} + + + )} + + + + {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ + theJob.v_make_desc || "" + } ${theJob.v_model_desc || ""}`} + + + {theJob.clm_total} + + + {theJob.actual_in} + + + {theJob.scheduled_completion} + + + - )} + + {!bodyshop.uselocalmediaserver && ( + + )} +
)} diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index fbeb8740d..4a1c85036 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -5,7 +5,7 @@ export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql` $offset: Int $limit: Int $order: [jobs_order_by!] - $statuses: [String!]!, + $statuses: [String!]! $isConverted: Boolean ) { jobs( @@ -120,7 +120,9 @@ export const QUERY_PARTS_QUEUE = gql` } } jobs( - where: { _and: [{ status: { _in: $statuses }, converted: { _eq: true } }] } + where: { + _and: [{ status: { _in: $statuses }, converted: { _eq: true } }] + } offset: $offset limit: $limit order_by: $order @@ -336,6 +338,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` category iouparent ro_number + ownerid ownr_fn ownr_ln ownr_co_nm diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 09798f0a5..f81d5ff0b 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1704,6 +1704,7 @@ "estimator": "Estimator", "filehandler": "File Handler", "insurance": "Insurance Details", + "more": "More", "notes": "Notes", "parts": "Parts", "totals": "Totals", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index da9f6b4e0..710db723d 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1704,6 +1704,7 @@ "estimator": "Estimador", "filehandler": "File Handler", "insurance": "detalles del seguro", + "more": "Más", "notes": "Notas", "parts": "Partes", "totals": "Totales", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a4c1dc686..0094ac983 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1704,6 +1704,7 @@ "estimator": "Estimateur", "filehandler": "Gestionnaire de fichiers", "insurance": "Détails de l'assurance", + "more": "Plus", "notes": "Remarques", "parts": "les pièces", "totals": "Totaux", From 23c0f8e383a588dc153a41994fdefeacafc1fc2f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 18 Jan 2024 16:07:17 -0500 Subject: [PATCH 017/131] - update handleBeta Signed-off-by: Dave Richer --- client/src/App/App.jsx | 7 +------ client/src/utils/handleBeta.js | 6 ++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 7d396eef8..7a4f72c41 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -54,17 +54,12 @@ export function App({ }) { const client = useClient(); - // Handle The Beta Switch. - useEffect(() => { - handleBeta(); - }, []) - - useEffect(() => { if (!navigator.onLine) { setOnline(false); } + handleBeta(); checkUserSession(); }, [checkUserSession, setOnline]); diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js index 428d4fb49..8a1fba468 100644 --- a/client/src/utils/handleBeta.js +++ b/client/src/utils/handleBeta.js @@ -24,12 +24,14 @@ export const handleBeta = () => { // Beta is enabled, but the current host name does start with beta. if (isBeta && !currentHostName.startsWith('beta')) { - window.location.href = `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; + const href= `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.replace(href); } // Beta is not enabled, but the current host name does start with beta. else if (!isBeta && currentHostName.startsWith('beta')) { - window.location.href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; + const href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.replace(href); } } export default handleBeta; From e0e62a52be9d082eaa34d355818b8dbf57a4286e Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 18 Jan 2024 13:08:39 -0800 Subject: [PATCH 018/131] Cherry pick schema chengs from IO-2477 --- hasura/metadata/tables.yaml | 74 +++++++++++++++++++ .../down.sql | 1 + .../up.sql | 18 +++++ .../down.sql | 1 + .../up.sql | 18 +++++ 5 files changed, 112 insertions(+) create mode 100644 hasura/migrations/1705522419599_create_table_public_eulas/down.sql create mode 100644 hasura/migrations/1705522419599_create_table_public_eulas/up.sql create mode 100644 hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql create mode 100644 hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 9867b3f8e..f4e4d99a7 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2423,6 +2423,73 @@ _eq: X-Hasura-User-Id - active: _eq: true +- table: + name: eula_acceptances + schema: public + object_relationships: + - name: eula + using: + foreign_key_constraint_on: eulaid + - name: user + using: + foreign_key_constraint_on: useremail + insert_permissions: + - role: user + permission: + check: + user: + authid: + _eq: X-Hasura-User-Id + columns: + - address + - buisness_name + - date_accepted + - eulaid + - first_name + - last_name + - phone_number + - useremail + select_permissions: + - role: user + permission: + columns: + - address + - buisness_name + - first_name + - last_name + - phone_number + - useremail + - created_at + - date_accepted + - updated_at + - eulaid + - id + filter: + user: + authid: + _eq: X-Hasura-User-Id +- table: + name: eulas + schema: public + array_relationships: + - name: eula_acceptances + using: + foreign_key_constraint_on: + column: eulaid + table: + name: eula_acceptances + schema: public + select_permissions: + - role: user + permission: + columns: + - id + - created_at + - updated_at + - effective_date + - end_date + - content + filter: {} - table: name: exportlog schema: public @@ -5888,6 +5955,13 @@ table: name: email_audit_trail schema: public + - name: eula_acceptances + using: + foreign_key_constraint_on: + column: useremail + table: + name: eula_acceptances + schema: public - name: exportlogs using: foreign_key_constraint_on: diff --git a/hasura/migrations/1705522419599_create_table_public_eulas/down.sql b/hasura/migrations/1705522419599_create_table_public_eulas/down.sql new file mode 100644 index 000000000..bea9117e2 --- /dev/null +++ b/hasura/migrations/1705522419599_create_table_public_eulas/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."eulas"; diff --git a/hasura/migrations/1705522419599_create_table_public_eulas/up.sql b/hasura/migrations/1705522419599_create_table_public_eulas/up.sql new file mode 100644 index 000000000..31eaa5f8d --- /dev/null +++ b/hasura/migrations/1705522419599_create_table_public_eulas/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."eulas" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "effective_date" timestamptz NOT NULL, "end_date" timestamptz, "content" text NOT NULL, PRIMARY KEY ("id") ); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_eulas_updated_at" +BEFORE UPDATE ON "public"."eulas" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_eulas_updated_at" ON "public"."eulas" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql new file mode 100644 index 000000000..29d08ee95 --- /dev/null +++ b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."eula_acceptances"; diff --git a/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql new file mode 100644 index 000000000..18dc09e8e --- /dev/null +++ b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."eula_acceptances" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "eulaid" uuid NOT NULL, "date_accepted" timestamptz NOT NULL, "first_name" text NOT NULL, "last_name" text NOT NULL, "address" text NOT NULL, "phone_number" Text NOT NULL, "buisness_name" Text NOT NULL, "useremail" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("eulaid") REFERENCES "public"."eulas"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("useremail") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_eula_acceptances_updated_at" +BEFORE UPDATE ON "public"."eula_acceptances" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_eula_acceptances_updated_at" ON "public"."eula_acceptances" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; From 829e6116922665c198cf858f99e48f5b3f258012 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 18 Jan 2024 16:16:06 -0500 Subject: [PATCH 019/131] - update handleBeta Signed-off-by: Dave Richer --- client/src/App/App.jsx | 3 +-- client/src/utils/handleBeta.js | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 7a4f72c41..2aae995c8 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -58,9 +58,8 @@ export function App({ if (!navigator.onLine) { setOnline(false); } - - handleBeta(); checkUserSession(); + handleBeta(); }, [checkUserSession, setOnline]); //const b = Grid.useBreakpoint(); diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js index 8a1fba468..e1d8199c4 100644 --- a/client/src/utils/handleBeta.js +++ b/client/src/utils/handleBeta.js @@ -26,12 +26,14 @@ export const handleBeta = () => { if (isBeta && !currentHostName.startsWith('beta')) { const href= `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; window.location.replace(href); + window.location.reload(true); } // Beta is not enabled, but the current host name does start with beta. else if (!isBeta && currentHostName.startsWith('beta')) { const href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; window.location.replace(href); + window.location.reload(true) } } export default handleBeta; From 0cab47f9849be095c82be5cd14eb12a4e89e836e Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 18 Jan 2024 16:22:58 -0500 Subject: [PATCH 020/131] - update handleBeta Signed-off-by: Dave Richer --- client/src/App/App.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 2aae995c8..2def5eaf2 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -59,7 +59,6 @@ export function App({ setOnline(false); } checkUserSession(); - handleBeta(); }, [checkUserSession, setOnline]); //const b = Grid.useBreakpoint(); @@ -108,6 +107,8 @@ export function App({ /> ); + handleBeta(); + return ( }> From bf6b1c202fa4292333302fcaae28da97b1ee5554 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 18 Jan 2024 16:35:30 -0500 Subject: [PATCH 021/131] - update handleBeta Signed-off-by: Dave Richer --- client/src/App/App.jsx | 1 + client/src/utils/handleBeta.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 2def5eaf2..4f074e5e9 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -73,6 +73,7 @@ export function App({ window.addEventListener("online", function (e) { setOnline(true); }); + useEffect(() => { if (currentUser.authorized && bodyshop) { client.setAttribute("imexshopid", bodyshop.imexshopid); diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js index e1d8199c4..8a1fba468 100644 --- a/client/src/utils/handleBeta.js +++ b/client/src/utils/handleBeta.js @@ -26,14 +26,12 @@ export const handleBeta = () => { if (isBeta && !currentHostName.startsWith('beta')) { const href= `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; window.location.replace(href); - window.location.reload(true); } // Beta is not enabled, but the current host name does start with beta. else if (!isBeta && currentHostName.startsWith('beta')) { const href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; window.location.replace(href); - window.location.reload(true) } } export default handleBeta; From cb8632641e0894fa1e7973e6bc13067297209bea Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 18 Jan 2024 17:28:46 -0800 Subject: [PATCH 022/131] IO-2608 Filter and Sort columns in Employee table --- .../shop-employees-list.component.jsx | 77 ++++++++++++++++--- client/src/graphql/employees.queries.js | 7 +- client/src/translations/en_us/common.json | 3 + client/src/translations/es/common.json | 3 + client/src/translations/fr/common.json | 3 + 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index 416e875f9..dd540745f 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -1,14 +1,20 @@ import { Button, Table } from "antd"; import queryString from "query-string"; -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation } from "react-router-dom"; +import { alphaSort } from "../../utils/sorters"; export default function ShopEmployeesListComponent({ loading, employees }) { const { t } = useTranslation(); const history = useHistory(); const search = queryString.parse(useLocation().search); + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" }, + }); + const handleOnRowClick = (record) => { if (record) { search.employeeId = record.id; @@ -18,32 +24,82 @@ export default function ShopEmployeesListComponent({ loading, employees }) { history.push({ search: queryString.stringify(search) }); } }; + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + const columns = [ { title: t("employees.fields.employee_number"), dataIndex: "employee_number", key: "employee_number", + sorter: (a, b) => alphaSort(a.employee_number, b.employee_number), + sortOrder: + state.sortedInfo.columnKey === "employee_number" && + state.sortedInfo.order, }, { - title: t("employees.fields.first_name"), - dataIndex: "first_name", - key: "first_name", + title: t("employees.labels.name"), + dataIndex: "employee_name", + key: "employee_name", + sorter: (a, b) => + alphaSort( + `${a.first_name || ""} ${a.last_name || ""}`.trim(), + `${b.first_name || ""} ${b.last_name || ""}`.trim() + ), + sortOrder: + state.sortedInfo.columnKey === "employee_name" && + state.sortedInfo.order, + render: (text, record) => + `${record.first_name || ""} ${record.last_name || ""}`.trim(), }, - { - title: t("employees.fields.last_name"), - dataIndex: "last_name", - key: "last_name", - }, - { title: t("employees.labels.rate_type"), dataIndex: "rate_type", key: "rate_type", + sorter: (a, b) => Number(a.flat_rate) - Number(b.flat_rate), + sortOrder: + state.sortedInfo.columnKey === "rate_type" && state.sortedInfo.order, + filters: [ + { + text: t("employees.labels.flat_rate"), + value: true, + }, + { + text: t("employees.labels.straight_time"), + value: false, + }, + ], + onFilter: (value, record) => value === record.flate_rate, render: (text, record) => record.flat_rate ? t("employees.labels.flat_rate") : t("employees.labels.straight_time"), }, + { + title: t("employees.labels.status"), + dataIndex: "active", + key: "active", + sorter: (a, b) => Number(a.active) - Number(b.active), + sortOrder: + state.sortedInfo.columnKey === "active" && state.sortedInfo.order, + filters: [ + { + text: t("employees.labels.active"), + value: true, + }, + { + text: t("employees.labels.inactive"), + value: false, + }, + ], + onFilter: (value, record) => value === record.active, + render: (text, record) => + record.active + ? t("employees.labels.active") + : t("employees.labels.inactive"), + }, ]; return (
@@ -74,6 +130,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) { type: "radio", selectedRowKeys: [search.employeeId], }} + onChange={handleTableChange} onRow={(record, rowIndex) => { return { onClick: (event) => { diff --git a/client/src/graphql/employees.queries.js b/client/src/graphql/employees.queries.js index 207c10bab..e34d2be3b 100644 --- a/client/src/graphql/employees.queries.js +++ b/client/src/graphql/employees.queries.js @@ -3,11 +3,12 @@ import { gql } from "@apollo/client"; export const QUERY_EMPLOYEES = gql` query QUERY_EMPLOYEES { employees(order_by: { employee_number: asc }) { - last_name - id + active + employee_number first_name flat_rate - employee_number + id + last_name } } `; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 09798f0a5..4842927d6 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1001,10 +1001,13 @@ }, "labels": { "actions": "Actions", + "active": "Active", "endmustbeafterstart": "End date must be after start date.", "flat_rate": "Flat Rate", + "inactive": "Inactive", "name": "Name", "rate_type": "Rate Type", + "status": "Status", "straight_time": "Straight Time" }, "successes": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index da9f6b4e0..4e96502e2 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1001,10 +1001,13 @@ }, "labels": { "actions": "", + "active": "", "endmustbeafterstart": "", "flat_rate": "", + "inactive": "", "name": "", "rate_type": "", + "status": "", "straight_time": "" }, "successes": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a4c1dc686..707f2f16a 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1001,10 +1001,13 @@ }, "labels": { "actions": "", + "active": "", "endmustbeafterstart": "", "flat_rate": "", + "inactive": "", "name": "", "rate_type": "", + "status": "", "straight_time": "" }, "successes": { From d06037df1fea4e825e808173570bd2717c88ff7c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 19 Jan 2024 08:47:17 -0800 Subject: [PATCH 023/131] IO-2598 Restrict IOU from Tech Console --- .../job-create-iou.component.jsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/src/components/job-create-iou/job-create-iou.component.jsx b/client/src/components/job-create-iou/job-create-iou.component.jsx index 8ebc8fb75..930f27e25 100644 --- a/client/src/components/job-create-iou/job-create-iou.component.jsx +++ b/client/src/components/job-create-iou/job-create-iou.component.jsx @@ -7,21 +7,31 @@ import { connect } from "react-redux"; import { useHistory } from "react-router"; import { createStructuredSelector } from "reselect"; import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, + technician: selectTechnician, }); + const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU); -export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) { +export function JobCreateIOU({ + bodyshop, + currentUser, + job, + selectedJobLines, + technician, +}) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const client = useApolloClient(); @@ -79,13 +89,19 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) { title={t("jobs.labels.createiouwarning")} onConfirm={handleCreateIou} disabled={ - !selectedJobLines || selectedJobLines.length === 0 || !job.converted + !selectedJobLines || + selectedJobLines.length === 0 || + !job.converted || + technician } >
+ + + + {lifecycleData.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent); diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 001a18166..a7d51d5e3 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -54,6 +54,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import UndefinedToNull from "../../utils/undefinedtonull"; import { DateTimeFormat } from "./../../utils/DateFormatter"; +import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -288,6 +289,13 @@ export function JobsDetailPage({ form={form} /> + Lifecycle} + key="lifecycle" + > + + { const {jobids} = req.body; @@ -11,11 +12,17 @@ const handleMultipleJobs = (jobIDs, req, res) => { return res.status(200).send(jobIDs); } -const handleSingleJob = (req, res) => { - +const handleSingleJob = async (jobIds, req, res) => { const client = req.userGraphQLClient; - return res.status(200).send(req.body); + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,}); + + const response = { + jobIds, + data: resp + } + + return res.status(200).json(response); } module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js index c766ceda5..423fbc73f 100644 --- a/server/middleware/eventAuthorizationMIddleware.js +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -1,3 +1,11 @@ +const path = require("path"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + /** * Checks if the event secret is correct * It adds the following properties to the request object: diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js index e11187041..9b4a5e9f6 100644 --- a/server/routes/jobRoutes.js +++ b/server/routes/jobRoutes.js @@ -7,14 +7,12 @@ const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebas const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job"); const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); -router.use(validateFirebaseIdTokenMiddleware); - -router.post('/totals', withUserGraphQLClientMiddleware, totals); +router.post('/totals', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals); router.post('/statustransition', eventAuthorizationMiddleware, statustransition); -router.post('/totalsssu', withUserGraphQLClientMiddleware,totalsSsu); -router.post('/costing', withUserGraphQLClientMiddleware,costing); -router.get('/lifecycle', withUserGraphQLClientMiddleware, lifecycle); -router.post('/costingmulti', withUserGraphQLClientMiddleware, costingmulti); -router.post('/partsscan', withUserGraphQLClientMiddleware, partsScan); +router.post('/totalsssu', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,totalsSsu); +router.post('/costing', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,costing); +router.post('/lifecycle', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, lifecycle); +router.post('/costingmulti', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti); +router.post('/partsscan', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan); module.exports = router; From cfe072744711fb98826e1b43ce70383e62278586 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 10:20:26 -0500 Subject: [PATCH 039/131] - Rough in front end / backend Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 34 +++++++++++-------- .../eventAuthorizationMIddleware.js | 6 ---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index dc6925055..c9303177e 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -13,7 +13,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export function JobLifecycleComponent({bodyshop, job, ...rest}) { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -56,20 +56,26 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { return ( - - -
+ {!loading ? ( + + +
+ + + + {lifecycleData.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + ) : ( + + Loading Job Timelines.... - - - {lifecycleData.map((item, index) => ( - - {item.value} - {new Date(item.start).toLocaleString()} - - ))} - - - + )} ); } diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js index 423fbc73f..9dd4dfd3a 100644 --- a/server/middleware/eventAuthorizationMIddleware.js +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -1,10 +1,4 @@ const path = require("path"); -require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), -}); /** * Checks if the event secret is correct From f59bdf90303fb6a931c7fa6d16424d6a2da682df Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:35:58 -0500 Subject: [PATCH 040/131] - Progress Update Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 100 +++++++++++++++--- server/graphql-client/queries.js | 5 +- server/job/job-lifecycle.js | 75 ++++++++++--- 3 files changed, 147 insertions(+), 33 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index c9303177e..3d356c575 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,9 +1,10 @@ import {createStructuredSelector} from "reselect"; import {selectBodyshop} from "../../redux/user/user.selectors"; import {connect} from "react-redux"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import axios from "axios"; import {Card, Space, Table, Timeline} from "antd"; +import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -12,6 +13,9 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); +const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; + + export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -24,8 +28,8 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { const response = await axios.post("/job/lifecycle", { jobids: job.id, }); - console.dir(response.data.data.transitions, {depth: null}); - setLifecycleData(response.data.data.transitions); + const data = response.data.transition[job.id]; + setLifecycleData(data); setLoading(false); } } @@ -36,6 +40,11 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { }); }, [job]); + // // TODO - Delete this useEffect, it is for testing + // useEffect(() => { + // console.dir(lifecycleData) + // }, [lifecycleData]); + const columnKeys = [ 'start', 'end', @@ -54,23 +63,82 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { key: key, })); + /** + * Returns an array of cells for the Pie Chart + * @type {function(): *[]} + */ + const renderCells = useCallback(() => { + const entires = Object + .entries(lifecycleData.durations) + .filter(([name, value]) => { + return value !== 0; + }) + + return entires.map(([name, value], index) => ( + + + + + )); + }, [lifecycleData, job]); + + /** + * Returns an array of objects with the name and value of the duration + * @type {function(): {name: *, value}[]} + */ + const durationsData = useCallback(() => { + return Object.entries(lifecycleData.durations) .filter(([name, value]) => { + return value !== 0; + }).map(([name, value]) => ({ + name, + value: value / 1000 + })) + }, [lifecycleData, job]); + return ( {!loading ? ( - - -
+ lifecycleData ? ( + + +
+ + + + + {lifecycleData.lifecycle.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {renderCells()} + + + + + + + + + ) : ( + + There is currently no lifecycle data for this job. - - - {lifecycleData.map((item, index) => ( - - {item.value} - {new Date(item.start).toLocaleString()} - - ))} - - - + ) ) : ( Loading Job Timelines.... diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a642a4988..782bd3100 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -518,8 +518,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` } }`; -exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: uuid!) { - transitions(where: {jobid: {_eq: $jobid}}, order_by: {id: asc}) { +exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { start end value @@ -529,6 +529,7 @@ exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: u type created_at updated_at + jobid } }`; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 207a514e3..8eddd86a5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,28 +1,73 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); -const jobLifecycle = (req, res) => { + +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + + // If there is no prev_value, it is the first transition + if (!transition.prev_value) { + statusDuration[transition.value] = duration; + } + // If there is no next_value, it is the last transition (the active one) + else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + // For all other transitions + else { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + }); + + return statusDuration; +} + + +const jobLifecycle = async (req, res) => { const {jobids} = req.body; - return _.isArray(jobids) ? - handleMultipleJobs(jobids, req, res) : - handleSingleJob(jobids, req, res); -}; + const jobIDs = _.isArray(jobids) ? jobids : [jobids]; -const handleMultipleJobs = (jobIDs, req, res) => { - return res.status(200).send(jobIDs); -} - -const handleSingleJob = async (jobIds, req, res) => { const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,}); + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); + + const transitions = resp.transitions; + + if (!transitions) { + return res.status(200).json({ + jobIDs, + transitions: [] + }); - const response = { - jobIds, - data: resp } - return res.status(200).json(response); + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); + + const groupedTransitions = {}; + + for (let jobId in transitionsByJobId) { + groupedTransitions[jobId] = { + lifecycle: transitionsByJobId[jobId], + durations: calculateStatusDuration(transitionsByJobId[jobId]) + }; + } + + return res.status(200).json({ + jobIDs, + transition: groupedTransitions, + }); } + module.exports = jobLifecycle; \ No newline at end of file From 5de4ef5d837a104ccf018b598b38fab4b87e1fe5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:54:38 -0500 Subject: [PATCH 041/131] - human readable dates Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 10 +++++---- server/job/job-lifecycle.js | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 3d356c575..626d26545 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -54,7 +54,9 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { 'duration', 'type', 'created_at', - 'updated_at' + 'updated_at', + 'start_readable', + 'end_readable', ]; const columns = columnKeys.map(key => ({ @@ -71,7 +73,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { const entires = Object .entries(lifecycleData.durations) .filter(([name, value]) => { - return value !== 0; + return value > 0; }) return entires.map(([name, value], index) => ( @@ -88,7 +90,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { */ const durationsData = useCallback(() => { return Object.entries(lifecycleData.durations) .filter(([name, value]) => { - return value !== 0; + return value > 0; }).map(([name, value]) => ({ name, value: value / 1000 @@ -108,7 +110,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { {lifecycleData.lifecycle.map((item, index) => ( - {item.value} - {new Date(item.start).toLocaleString()} + {item.value} - {item.start_readable} ))} diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 8eddd86a5..bc24e9c8a 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,5 +1,6 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); +const moment = require("moment"); const calculateStatusDuration = (transitions) => { let statusDuration = {}; @@ -52,17 +53,30 @@ const jobLifecycle = async (req, res) => { } + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; - + moment.relativeTimeThreshold('m', 30) for (let jobId in transitionsByJobId) { + let lifecycle = transitionsByJobId[jobId].map(transition => { + if (transition.start) { + transition.start_readable = moment(transition.start).fromNow(); + } + if (transition.end) { + transition.end_readable = moment(transition.end).fromNow(); + } + return transition; + }); + groupedTransitions[jobId] = { - lifecycle: transitionsByJobId[jobId], - durations: calculateStatusDuration(transitionsByJobId[jobId]) + lifecycle: lifecycle, + durations: calculateStatusDuration(lifecycle) }; } - + + console.dir(groupedTransitions, {depth: null}); + return res.status(200).json({ jobIDs, transition: groupedTransitions, From d0a2bb7da0610f2773dbb3abb8dc66966334eb17 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:58:57 -0500 Subject: [PATCH 042/131] - human readable dates Signed-off-by: Dave Richer --- client/src/components/job-lifecycle/job-lifecycle.component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 626d26545..8122229af 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -15,7 +15,6 @@ const mapDispatchToProps = (dispatch) => ({ const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; - export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); From d740446ccb7ecbe7494f4e8e2f76e40c0c26d6a7 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 10:07:07 -0500 Subject: [PATCH 043/131] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 165 ++++++++---------- .../jobs-detail.page.component.jsx | 2 +- server/job/job-lifecycle.js | 31 +++- 3 files changed, 92 insertions(+), 106 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 8122229af..62ecb920a 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,139 +1,110 @@ -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {connect} from "react-redux"; -import {useCallback, useEffect, useState} from "react"; -import axios from "axios"; -import {Card, Space, Table, Timeline} from "antd"; -import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts"; +import React, {useEffect, useMemo, useState} from 'react'; +import axios from 'axios'; +import {Card, Space, Table, Timeline} from 'antd'; +import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); +export function JobLifecycleComponent({job, ...rest}) { -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; - -export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - async function getLifecycleData() { + const getLifecycleData = async () => { if (job && job.id) { - setLoading(true); - const response = await axios.post("/job/lifecycle", { - jobids: job.id, - }); - const data = response.data.transition[job.id]; - setLifecycleData(data); - setLoading(false); + try { + setLoading(true); + const response = await axios.post("/job/lifecycle", {jobids: job.id}); + const data = response.data.transition[job.id]; + setLifecycleData(data); + } catch (err) { + console.error(`Error getting Job Lifecycle Data: ${err.message}`); + } finally { + setLoading(false); + } } - } + }; - getLifecycleData().catch((err) => { - console.log(`Something went wrong getting Job Lifecycle Data: ${err.message}`); - setLoading(false); - }); + getLifecycleData(); }, [job]); - // // TODO - Delete this useEffect, it is for testing - // useEffect(() => { - // console.dir(lifecycleData) - // }, [lifecycleData]); - const columnKeys = [ - 'start', - 'end', - 'value', - 'prev_value', - 'next_value', - 'duration', - 'type', - 'created_at', - 'updated_at', - 'start_readable', - 'end_readable', + 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration' ]; const columns = columnKeys.map(key => ({ - title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the first letter for the title + title: key.charAt(0).toUpperCase() + key.slice(1), dataIndex: key, key: key, })); - /** - * Returns an array of cells for the Pie Chart - * @type {function(): *[]} - */ - const renderCells = useCallback(() => { - const entires = Object - .entries(lifecycleData.durations) - .filter(([name, value]) => { - return value > 0; - }) - return entires.map(([name, value], index) => ( - - - - - )); - }, [lifecycleData, job]); + const durationsData = useMemo(() => { + if (!lifecycleData) { + return []; + } - /** - * Returns an array of objects with the name and value of the duration - * @type {function(): {name: *, value}[]} - */ - const durationsData = useCallback(() => { - return Object.entries(lifecycleData.durations) .filter(([name, value]) => { - return value > 0; - }).map(([name, value]) => ({ - name, - value: value / 1000 - })) - }, [lifecycleData, job]); + const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => { + return { + name, + amt: value, + pv: humanReadable, + uv: value, + } + }) + + return [transformedData]; + }, [lifecycleData]); + + + useEffect(() => { + console.dir(lifecycleData, {depth: null}) + console.dir(durationsData, {depth: null}) + + }, [lifecycleData,durationsData]); return ( {!loading ? ( lifecycleData ? ( - -
- {lifecycleData.lifecycle.map((item, index) => ( - + {item.value} - {item.start_readable} ))} - - `${name}: ${(percent * 100).toFixed(0)}%`} - outerRadius={80} - fill="#8884d8" - dataKey="value" + + - {renderCells()} - - - - + + + + + + + + + - + +
+ ) : ( @@ -149,4 +120,4 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent); +export default JobLifecycleComponent; \ No newline at end of file diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index a7d51d5e3..0f335d982 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -294,7 +294,7 @@ export function JobsDetailPage({ tab={Lifecycle} key="lifecycle" > - + { let statusDuration = {}; transitions.forEach((transition, index) => { - let duration = transition.duration; + let duration = transition.duration_minutes; // If there is no prev_value, it is the first transition if (!transition.prev_value) { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } // If there is no next_value, it is the last transition (the active one) else if (!transition.next_value) { if (statusDuration[transition.value]) { - statusDuration[transition.value] += duration; + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; } else { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } } // For all other transitions else { if (statusDuration[transition.value]) { - statusDuration[transition.value] += duration; + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; } else { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } } }); @@ -53,7 +64,6 @@ const jobLifecycle = async (req, res) => { } - const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; @@ -66,6 +76,11 @@ const jobLifecycle = async (req, res) => { if (transition.end) { transition.end_readable = moment(transition.end).fromNow(); } + if(transition.duration){ + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + transition.duration_readable = moment.duration(transition.duration).humanize(); + } return transition; }); @@ -75,7 +90,7 @@ const jobLifecycle = async (req, res) => { }; } - console.dir(groupedTransitions, {depth: null}); + console.dir(groupedTransitions, {depth: null}) return res.status(200).json({ jobIDs, From 5ea64ed805533994799e4e023a21dd84905344b1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 17:18:43 -0500 Subject: [PATCH 044/131] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 178 +++++++++++------- client/src/utils/DateFormatter.jsx | 3 + server/graphql-client/queries.js | 2 +- server/job/job-lifecycle.js | 74 ++------ server/utils/calculateStatusDuration.js | 59 ++++++ server/utils/durationToHumanReadable.js | 22 +++ 6 files changed, 215 insertions(+), 123 deletions(-) create mode 100644 server/utils/calculateStatusDuration.js create mode 100644 server/utils/durationToHumanReadable.js diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 62ecb920a..3f48e826e 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,89 +1,135 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import moment from "moment"; import axios from 'axios'; -import {Card, Space, Table, Timeline} from 'antd'; -import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {Card, Space, Table} from 'antd'; +import {gql, useQuery} from "@apollo/client"; +import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; +import {isEmpty} from "lodash"; +import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; + +const transformDataForChart = (durations) => { + const output = {}; + output.total = durations.total; + return durations.summations.forEach((summation) => { + output[summation.status] = summation.value + }); +}; +const getColor = (key) => { + // Generate a random color + const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16); + return randomColor; +}; + export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - const getLifecycleData = async () => { - if (job && job.id) { - try { - setLoading(true); - const response = await axios.post("/job/lifecycle", {jobids: job.id}); - const data = response.data.transition[job.id]; - setLifecycleData(data); - } catch (err) { - console.error(`Error getting Job Lifecycle Data: ${err.message}`); - } finally { - setLoading(false); - } + // Used for tracking external state changes. + const {data} = useQuery(gql` + query get_job_test($id: uuid!){ + jobs_by_pk(id:$id){ + id + status } - }; + } + `, { + variables: { + id: job.id + }, + fetchPolicy: 'cache-only' + }); - getLifecycleData(); + /** + * Gets the lifecycle data for the job. + * @returns {Promise} + */ + const getLifecycleData = useCallback(async () => { + if (job && job.id) { + try { + setLoading(true); + const response = await axios.post("/job/lifecycle", {jobids: job.id}); + const data = response.data.transition[job.id]; + setLifecycleData(data); + } catch (err) { + console.error(`Error getting Job Lifecycle Data: ${err.message}`); + } finally { + setLoading(false); + } + } }, [job]); - const columnKeys = [ - 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration' + useEffect(() => { + if (!data) return; + setTimeout(() => { + getLifecycleData().catch(err => console.error(`Error getting Job Lifecycle Data: ${err.message}`)); + }, 1000); + }, [data, getLifecycleData]); + + const columns = [ + { + title: 'Value', + dataIndex: 'value', + key: 'value', + }, + { + title: 'Start', + dataIndex: 'start', + key: 'start', + render: (text) => DateTimeFormatterFunction(text), + sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(), + }, + { + title: 'Relative Start', + dataIndex: 'start_readable', + key: 'start_readable', + }, + { + title: 'End', + dataIndex: 'end', + key: 'end', + sorter: (a, b) => { + if (isEmpty(a.end) || isEmpty(b.end)) { + if (isEmpty(a.end) && isEmpty(b.end)) { + return 0; + } + return isEmpty(a.end) ? 1 : -1; + } + return moment(a.end).unix() - moment(b.end).unix(); + }, + render: (text) => isEmpty(text) ? 'N/A' : DateTimeFormatterFunction(text) + }, + { + title: 'Relative End', + dataIndex: 'end_readable', + key: 'end_readable', + }, + { + title: 'Duration', + dataIndex: 'duration_readable', + key: 'duration_readable', + sorter: (a, b) => a.duration - b.duration, + }, ]; - const columns = columnKeys.map(key => ({ - title: key.charAt(0).toUpperCase() + key.slice(1), - dataIndex: key, - key: key, - })); - - - const durationsData = useMemo(() => { - if (!lifecycleData) { - return []; - } - - const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => { - return { - name, - amt: value, - pv: humanReadable, - uv: value, - } - }) - - return [transformedData]; - }, [lifecycleData]); - - useEffect(() => { + console.log('LifeCycle Data'); console.dir(lifecycleData, {depth: null}) - console.dir(durationsData, {depth: null}) - - }, [lifecycleData,durationsData]); + }, [lifecycleData]); return ( {!loading ? ( - lifecycleData ? ( + lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? ( - - - {lifecycleData.lifecycle.map((item, index) => ( - - {item.value} - {item.start_readable} - - ))} - - - - + {lifecycleData.durations.summations.map((summation, idx) => { + + return ( + + ); + })} + + - +
diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index d034266e3..e8137c54d 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -17,6 +17,9 @@ export function DateTimeFormatter(props) { ) : null; } +export function DateTimeFormatterFunction(date) { + return moment(date).format("MM/DD/YYYY hh:mm a"); +} export function TimeFormatter(props) { return props.children ? moment(props.children).format(props.format ? props.format : "hh:mm a") diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 782bd3100..2bd83d1ab 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -519,7 +519,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` }`; exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { - transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) { start end value diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 07aaa037a..60d0c0be5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,57 +1,14 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); const moment = require("moment"); - -const calculateStatusDuration = (transitions) => { - let statusDuration = {}; - - transitions.forEach((transition, index) => { - let duration = transition.duration_minutes; - - // If there is no prev_value, it is the first transition - if (!transition.prev_value) { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - // If there is no next_value, it is the last transition (the active one) - else if (!transition.next_value) { - if (statusDuration[transition.value]) { - statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - // For all other transitions - else { - if (statusDuration[transition.value]) { - statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - }); - - return statusDuration; -} - +const durationToHumanReadable = require("../utils/durationToHumanReadable"); +const calculateStatusDuration = require("../utils/calculateStatusDuration"); const jobLifecycle = async (req, res) => { const {jobids} = req.body; const jobIDs = _.isArray(jobids) ? jobids : [jobids]; - const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); const transitions = resp.transitions; @@ -67,19 +24,21 @@ const jobLifecycle = async (req, res) => { const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; - moment.relativeTimeThreshold('m', 30) + for (let jobId in transitionsByJobId) { let lifecycle = transitionsByJobId[jobId].map(transition => { - if (transition.start) { - transition.start_readable = moment(transition.start).fromNow(); - } - if (transition.end) { - transition.end_readable = moment(transition.end).fromNow(); - } - if(transition.duration){ - transition.duration_seconds = Math.round(transition.duration / 1000); - transition.duration_minutes = Math.round(transition.duration_seconds / 60); - transition.duration_readable = moment.duration(transition.duration).humanize(); + transition.start_readable = transition.start ? moment(transition.start).fromNow() : 'N/A'; + transition.end_readable = transition.end ? moment(transition.end).fromNow() : 'N/A'; + + if (transition.duration) { + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + let duration = moment.duration(transition.duration); + transition.duration_readable = durationToHumanReadable(duration); + } else { + transition.duration_seconds = 0; + transition.duration_minutes = 0; + transition.duration_readable = 'N/A'; } return transition; }); @@ -90,13 +49,10 @@ const jobLifecycle = async (req, res) => { }; } - console.dir(groupedTransitions, {depth: null}) - return res.status(200).json({ jobIDs, transition: groupedTransitions, }); } - module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js new file mode 100644 index 000000000..8a74e09f8 --- /dev/null +++ b/server/utils/calculateStatusDuration.js @@ -0,0 +1,59 @@ +const moment = require('moment'); +const durationToHumanReadable = require("./durationToHumanReadable"); +/** + * Calculate the duration of each status of a job + * @param transitions + * @returns {{}} + */ +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + let totalDuration = 0; + let summations = []; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + totalDuration += duration; + + if (!transition.prev_value) { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } else { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } + }); + + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { + if (status !== 'total') { + summations.push({status, value, humanReadable}); + } + } + + const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); + + return { + summations, + total: totalDuration, + humanReadableTotal + }; +} +module.exports = calculateStatusDuration; \ No newline at end of file diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js new file mode 100644 index 000000000..f13e24c98 --- /dev/null +++ b/server/utils/durationToHumanReadable.js @@ -0,0 +1,22 @@ +const durationToHumanReadable = (duration) => { + if (!duration) return 'N/A'; + + let parts = []; + + let years = duration.years(); + let months = duration.months(); + let days = duration.days(); + let hours = duration.hours(); + let minutes = duration.minutes(); + let seconds = duration.seconds(); + + if (years) parts.push(years + ' year' + (years > 1 ? 's' : '')); + if (months) parts.push(months + ' month' + (months > 1 ? 's' : '')); + if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); + if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); + if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); + if (!minutes && !hours && !days && !months && !years && seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + + return parts.join(', '); +} +module.exports = durationToHumanReadable; \ No newline at end of file From 6489a8666f8c4a6d94fc8be835a14392ea8ab853 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 18:39:59 -0500 Subject: [PATCH 045/131] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 67 +++++++++---------- server/utils/calculateStatusDuration.js | 2 +- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 3f48e826e..58475e300 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -5,24 +5,25 @@ import {Card, Space, Table} from 'antd'; import {gql, useQuery} from "@apollo/client"; import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; import {isEmpty} from "lodash"; -import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; +import {Bar, BarChart, CartesianGrid, Legend, Tooltip, YAxis} from "recharts"; + const transformDataForChart = (durations) => { const output = {}; - output.total = durations.total; - return durations.summations.forEach((summation) => { - output[summation.status] = summation.value - }); -}; + // output.amt = durations.total; + // output.name = 'Total'; + durations.summations.forEach((summation) => { + output[summation.status] = summation.value; + }); + return [output]; +} const getColor = (key) => { // Generate a random color - const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16); + const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); return randomColor; }; - export function JobLifecycleComponent({job, ...rest}) { - const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -125,33 +126,29 @@ export function JobLifecycleComponent({job, ...rest}) { - - - - - - - - {lifecycleData.durations.summations.map((summation, idx) => { - + + + + + + { + Object.keys(transformDataForChart(lifecycleData.durations)[0]).map((key) => { return ( - - ); - })} - - - - + + ) + }) + } + diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index 8a74e09f8..ae0ef8238 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -11,7 +11,7 @@ const calculateStatusDuration = (transitions) => { let summations = []; transitions.forEach((transition, index) => { - let duration = transition.duration; + let duration = transition.duration_minutes; totalDuration += duration; if (!transition.prev_value) { From 03d4e4dcd133129dc77dfca0e31807ecff20dbdd Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 16:01:48 -0800 Subject: [PATCH 046/131] IO-2543 AR Aging --- .../jobs-admin-remove-ar.component.jsx | 63 +++ .../report-center-modal.component.jsx | 38 +- client/src/graphql/jobs.queries.js | 450 +++++++++--------- .../src/pages/jobs-admin/jobs-admin.page.jsx | 5 +- client/src/translations/en_us/common.json | 3 + client/src/translations/es/common.json | 3 + client/src/translations/fr/common.json | 3 + client/src/utils/AuditTrailMappings.js | 66 +-- client/src/utils/TemplateConstants.js | 10 + 9 files changed, 366 insertions(+), 275 deletions(-) create mode 100644 client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx diff --git a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx new file mode 100644 index 000000000..c85404f48 --- /dev/null +++ b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx @@ -0,0 +1,63 @@ +import { gql, useMutation } from "@apollo/client"; +import { Form, Switch, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; + +const mapStateToProps = createStructuredSelector({}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR); + +export function JobsAdminRemoveAR({ insertAuditTrail, job }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [switchValue, setSwitchValue] = useState(job.remove_from_ar); + + const [updateJob] = useMutation(gql` + mutation REMOVE_FROM_AR_JOB($jobId: uuid!, $remove_from_ar: Boolean!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { remove_from_ar: $remove_from_ar } + ) { + id + remove_from_ar + } + } + `); + + const handleChange = async (value) => { + setLoading(true); + const result = await updateJob({ + variables: { jobId: job.id, remove_from_ar: value }, + }); + + if (!result.errors) { + notification["success"]({ message: t("jobs.successes.save") }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.admin_job_remove_from_ar(value), + }); + setSwitchValue(value); + } else { + notification["error"]({ + message: t("jobs.errors.saving", { + error: JSON.stringify(result.errors), + }), + }); + } + setLoading(false); + }; + + return ( + + + + ); +} diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index 913d27282..e37e02164 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -239,20 +239,30 @@ export function ReportCenterModalComponent({ reportCenterModal }) { else return null; }} - - + + {() => { + const key = form.getFieldValue("key"); + const datedisable = Templates[key] && Templates[key].datedisable; + if (datedisable !== true) { + return ( + + + + ); + } else return null; + }} {() => { diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 4a1c85036..adfc42765 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -545,147 +545,166 @@ export const QUERY_JOB_COSTING_DETAILS = gql` export const GET_JOB_BY_PK = gql` query GET_JOB_BY_PK($id: uuid!) { jobs_by_pk(id: $id) { - updated_at + actual_completion + actual_delivery + actual_in + adjustment_bottom_line + area_of_damage + auto_add_ats + available_jobs { + id + } + alt_transport + ca_bc_pvrt + ca_customer_gst + ca_gst_registrant + category + cccontracts { + agreementnumber + courtesycar { + fleetnumber + id + make + model + plate + year + } + id + scheduledreturn + start + status + } + cieca_ttl + class + clm_no + clm_total + comment + converted + csiinvites { + completedon + id + } + date_estimated + date_exported + date_invoiced + date_last_contacted + date_lost_sale + date_next_contact + date_open + date_rentalresp + date_repairstarted + date_scheduled + date_towin + date_void + ded_amt + ded_note + ded_status + deliverchecklist + depreciation_taxes + driveable + employee_body employee_body_rel { id first_name last_name } - employee_refinish_rel { - id - first_name - last_name - } - employee_prep_rel { - id - first_name - last_name - } + employee_csr employee_csr_rel { id first_name last_name } - employee_csr employee_prep + employee_prep_rel { + id + first_name + last_name + } employee_refinish - employee_body - alt_transport - intakechecklist - invoice_final_note - comment - loss_desc - kmin - kmout - referral_source - referral_source_extra - unit_number - po_number - special_coverage_policy - scheduled_delivery - converted - lbr_adjustments - ro_number - po_number - clm_total + employee_refinish_rel { + id + first_name + last_name + } + est_co_nm + est_ct_fn + est_ct_ln + est_ea + est_ph1 + federal_tax_rate + id inproduction - vehicleid - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - vehicleid - driveable - towin - loss_of_use - lost_sale_reason - vehicle { - id - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - notes - v_paint_codes - jobs { - id - ro_number - status - clm_no - } - } - available_jobs { - id - } - ins_co_id - policy_no - loss_date - clm_no - area_of_damage - ins_co_nm ins_addr1 ins_city + ins_co_id + ins_co_nm ins_ct_ln ins_ct_fn ins_ea ins_ph1 - est_co_nm - est_ct_fn - est_ct_ln - est_ph1 - est_ea - selling_dealer - servicing_dealer - selling_dealer_contact - servicing_dealer_contact - regie_number - scheduled_completion - id - ded_amt - ded_status - depreciation_taxes - other_amount_payable - towing_payable - storage_payable - adjustment_bottom_line - federal_tax_rate - state_tax_rate - local_tax_rate - tax_tow_rt - tax_str_rt - tax_paint_mat_rt - tax_shop_mat_rt - tax_sub_rt - tax_lbr_rt - tax_levies_rt - parts_tax_rates - job_totals - ownr_fn - ownr_ln - ownr_co_nm - ownr_ea - ownr_addr1 - ownr_addr2 - ownr_city - ownr_st - ownr_zip - ownr_ctry - ownr_ph1 - ownr_ph2 - production_vars - ca_gst_registrant - ownerid - ded_note - materials - auto_add_ats - rate_ats + intakechecklist + invoice_final_note iouparent + job_totals + joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + act_price + ah_detail_line + alt_partm + alt_partno + billlines(limit: 1, order_by: { bill: { date: desc } }) { + actual_cost + actual_price + bill { + id + invoice_number + vendor { + id + name + } + } + joblineid + id + quantity + } + convertedtolbr + critical + db_hrs + db_price + db_ref + id + ioucreated + lbr_amt + lbr_op + line_desc + line_ind + line_no + line_ref + location + manual_line + mod_lb_hrs + mod_lbr_ty + notes + oem_partno + op_code_desc + part_qty + part_type + prt_dsmk_m + prt_dsmk_p + status + tax_part + unq_seq + } + kmin + kmout + labor_rate_desc + lbr_adjustments + local_tax_rate + loss_date + loss_desc + loss_of_use + lost_sale_reason + materials + other_amount_payable owner { id ownr_fn @@ -702,7 +721,40 @@ export const GET_JOB_BY_PK = gql` ownr_ph2 tax_number } - labor_rate_desc + owner_owing + ownerid + ownr_addr1 + ownr_addr2 + ownr_ctry + ownr_city + ownr_co_nm + ownr_ea + ownr_fn + ownr_ln + ownr_ph1 + ownr_ph2 + ownr_st + ownr_zip + parts_tax_rates + payments { + amount + created_at + date + exportedat + id + jobid + memo + payer + paymentnum + transactionid + type + } + plate_no + plate_st + po_number + policy_no + production_vars + rate_ats rate_la1 rate_la2 rate_la3 @@ -726,121 +778,64 @@ export const GET_JOB_BY_PK = gql` rate_mapa rate_mash rate_matd - actual_in - federal_tax_rate - local_tax_rate - state_tax_rate + regie_number + referral_source + referral_source_extra + remove_from_ar + ro_number scheduled_completion - scheduled_in - actual_completion scheduled_delivery - actual_delivery - date_estimated - date_open - date_scheduled - date_invoiced - date_last_contacted - date_lost_sale - date_next_contact - date_towin - date_rentalresp - date_exported - date_repairstarted - date_void + scheduled_in + selling_dealer + servicing_dealer + selling_dealer_contact + servicing_dealer_contact + special_coverage_policy + state_tax_rate status - owner_owing - tax_registration_number - class - category - deliverchecklist - voided - ca_bc_pvrt - ca_customer_gst + storage_payable suspended - joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + tax_lbr_rt + tax_levies_rt + tax_paint_mat_rt + tax_registration_number + tax_shop_mat_rt + tax_str_rt + tax_sub_rt + tax_tow_rt + towin + towing_payable + unit_number + updated_at + v_vin + v_model_yr + v_model_desc + v_make_desc + v_color + vehicleid + vehicle { id - alt_partm - line_no - unq_seq - line_ind - line_desc - line_ref - part_type - oem_partno - alt_partno - db_price - act_price - part_qty - mod_lbr_ty - db_hrs - mod_lb_hrs - lbr_op - lbr_amt - op_code_desc - status + jobs { + clm_no + id + ro_number + status + } notes - location - tax_part - db_ref - manual_line - prt_dsmk_p - prt_dsmk_m - ioucreated - convertedtolbr - ah_detail_line - critical - billlines(limit: 1, order_by: { bill: { date: desc } }) { - id - quantity - actual_cost - actual_price - joblineid - bill { - id - invoice_number - vendor { - id - name - } - } - } - } - payments { - id - jobid - amount - payer - paymentnum - created_at - transactionid - memo - date - type - exportedat - } - cccontracts { - id - status - start - scheduledreturn - agreementnumber - courtesycar { - id - make - model - year - plate - fleetnumber - } - } - cieca_ttl - csiinvites { - id - completedon + plate_no + plate_st + v_color + v_make_desc + v_model_desc + v_model_yr + v_paint_codes + v_vin } + voided } } `; + export const GET_JOB_RECONCILIATION_BY_PK = gql` query GET_JOB_RECONCILIATION_BY_PK($id: uuid!) { bills(where: { jobid: { _eq: $id } }) { @@ -905,6 +900,7 @@ export const GET_JOB_RECONCILIATION_BY_PK = gql` } } `; + export const QUERY_JOB_CARD_DETAILS = gql` query QUERY_JOB_CARD_DETAILS($id: uuid!) { jobs_by_pk(id: $id) { diff --git a/client/src/pages/jobs-admin/jobs-admin.page.jsx b/client/src/pages/jobs-admin/jobs-admin.page.jsx index 0d08e5114..0c7eb9a33 100644 --- a/client/src/pages/jobs-admin/jobs-admin.page.jsx +++ b/client/src/pages/jobs-admin/jobs-admin.page.jsx @@ -7,16 +7,16 @@ import { useParams } from "react-router-dom"; import AlertComponent from "../../components/alert/alert.component"; import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component"; import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; +import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; import JobsAdminClass from "../../components/jobs-admin-class/jobs-admin-class.component"; import JobsAdminDatesChange from "../../components/jobs-admin-dates/jobs-admin-dates.component"; import JobsAdminDeleteIntake from "../../components/jobs-admin-delete-intake/jobs-admin-delete-intake.component"; import JobsAdminMarkReexport from "../../components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component"; import JobAdminOwnerReassociate from "../../components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component"; +import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-remove-ar.component"; import JobsAdminUnvoid from "../../components/jobs-admin-unvoid/jobs-admin-unvoid.component"; import JobAdminVehicleReassociate from "../../components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; - import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; @@ -104,6 +104,7 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) { + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 919f7e297..1589e9b5f 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "ADMIN: Remove from AR updated to: {{status}}", "admin_jobmarkexported": "ADMIN: Job marked as exported.", "admin_jobmarkforreexport": "ADMIN: Job marked for re-export.", "admin_jobuninvoice": "ADMIN: Job has been uninvoiced.", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "Parts & Sublet Reconciliation", "relatedros": "Related ROs", + "remove_from_ar": "Remove from AR", "returntotals": "Return Totals", "rosaletotal": "RO Parts Total", "sale_additional": "Sales - Additional", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "Anticipated Revenue", + "ar_aging": "AR Aging", "attendance_detail": "Attendance (All Employees)", "attendance_employee": "Employee Attendance", "attendance_summary": "Attendance Summary (All Employees)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index a798f790c..fc977e7eb 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b65abaf04..76e3ce6a9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index d7098fa2d..eefbb3a11 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -1,54 +1,56 @@ import i18n from "i18next"; const AuditTrailMapping = { - alertToggle: (status) => i18n.t("audit_trail.messages.alerttoggle", { status }), + admin_job_remove_from_ar: (status) => + i18n.t("audit_trail.messages.admin_job_remove_from_ar", { status }), + admin_jobfieldchange: (field, value) => + "ADMIN: " + + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + admin_jobmarkexported: () => + i18n.t("audit_trail.messages.admin_jobmarkexported"), + admin_jobmarkforreexport: () => + i18n.t("audit_trail.messages.admin_jobmarkforreexport"), + admin_jobstatuschange: (status) => + "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), + admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), + admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), + alertToggle: (status) => + i18n.t("audit_trail.messages.alerttoggle", { status }), appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }), - jobstatuschange: (status) => - i18n.t("audit_trail.messages.jobstatuschange", { status }), - admin_jobstatuschange: (status) => - "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), - jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), - jobimported: () => i18n.t("audit_trail.messages.jobimported"), - jobinvoiced: () => - i18n.t("audit_trail.messages.jobinvoiced"), - jobconverted: (ro_number) => - i18n.t("audit_trail.messages.jobconverted", { ro_number }), - jobfieldchange: (field, value) => - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - admin_jobfieldchange: (field, value) => - "ADMIN: " + - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - jobspartsorder: (order_number) => - i18n.t("audit_trail.messages.jobspartsorder", { order_number }), - jobspartsreturn: (order_number) => - i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), - jobmodifylbradj: ({ mod_lbr_ty, hours }) => - i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }), + failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), - jobinproductionchange: (inproduction) => - i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), jobchecklist: (type, inproduction, status) => i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), + jobconverted: (ro_number) => + i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobfieldchange: (field, value) => + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + jobimported: () => i18n.t("audit_trail.messages.jobimported"), + jobinproductionchange: (inproduction) => + i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), + jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"), + jobmodifylbradj: ({ mod_lbr_ty, hours }) => + i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"), - jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"), - admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), - admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), - admin_jobmarkforreexport: () => - i18n.t("audit_trail.messages.admin_jobmarkforreexport"), - admin_jobmarkexported: () => - i18n.t("audit_trail.messages.admin_jobmarkexported"), - failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), + jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), + jobspartsorder: (order_number) => + i18n.t("audit_trail.messages.jobspartsorder", { order_number }), + jobspartsreturn: (order_number) => + i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), + jobstatuschange: (status) => + i18n.t("audit_trail.messages.jobstatuschange", { status }), + jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), }; export default AuditTrailMapping; diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index eeb937c3a..ad614af3b 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2020,6 +2020,7 @@ export const TemplateList = (type, context) => { key: "lost_sales", //idtype: "vendor", disabled: false, + datedisable: true, rangeFilter: { object: i18n.t("reportcenter.labels.objects.jobs"), field: i18n.t("jobs.fields.date_lost_sale"), @@ -2039,6 +2040,15 @@ export const TemplateList = (type, context) => { }, group: "jobs", }, + ar_aging: { + title: i18n.t("reportcenter.templates.ar_aging"), + subject: i18n.t("reportcenter.templates.ar_aging"), + key: "ar_aging", + //idtype: "vendor", + disabled: false, + datedisable: true, + group: "customers", + }, } : {}), ...(!type || type === "courtesycarcontract" From 36dd97394f99db593026281ffff88a2d78c3a94c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 16:15:32 -0800 Subject: [PATCH 047/131] IO-2543 Adjust for having no dates --- .../report-center-modal/report-center-modal.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index e37e02164..c4bef11c3 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -68,8 +68,8 @@ export function ReportCenterModalComponent({ reportCenterModal }) { const handleFinish = async (values) => { setLoading(true); - const start = values.dates[0]; - const end = values.dates[1]; + const start = values.dates ? values.dates[0] : null; + const end = values.dates ? values.dates[1] : null; const { id } = values; await GenerateDocument( From eb8519dc1d0ddb982c58a6df0d180f8fedf1cb07 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 18:37:41 -0800 Subject: [PATCH 048/131] IO-2543 Move queries to jobs.queries.js and correct layout --- .../jobs-admin-change.status.component.jsx | 14 ++- .../jobs-admin-delete-intake.component.jsx | 56 ++++----- .../jobs-admin-mark-reexport.component.jsx | 118 +++++++----------- .../jobs-admin-remove-ar.component.jsx | 36 +++--- .../jobs-admin-unvoid.component.jsx | 76 +++-------- client/src/graphql/jobs.queries.js | 117 +++++++++++++++++ 6 files changed, 230 insertions(+), 187 deletions(-) diff --git a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx index edef81343..54a020d37 100644 --- a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx +++ b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx @@ -53,12 +53,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) { ); return ( - - - + + + + ); } diff --git a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx index 190778437..ec1bd976b 100644 --- a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx +++ b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx @@ -1,34 +1,18 @@ import { useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; -import { gql } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { + DELETE_DELIVERY_CHECKLIST, + DELETE_INTAKE_CHECKLIST, +} from "../../graphql/jobs.queries"; + export default function JobAdminDeleteIntake({ job }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [deleteIntake] = useMutation(gql` - mutation DELETE_INTAKE($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { intakechecklist: null } - ) { - id - intakechecklist - } - } - `); - const [DELETE_DELIVERY] = useMutation(gql` - mutation DELETE_DELIVERY($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { deliverchecklist: null } - ) { - id - deliverchecklist - } - } - `); + const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST); + const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST); const handleDelete = async (values) => { setLoading(true); @@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) { const handleDeleteDelivery = async (values) => { setLoading(true); - const result = await DELETE_DELIVERY({ + const result = await deleteDelivery({ variables: { jobId: job.id }, }); @@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) { return ( <> - - + + + + ); } diff --git a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx index c47c30def..ae193fa72 100644 --- a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx +++ b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx @@ -1,5 +1,5 @@ -import { gql, useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; +import { useMutation } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -7,6 +7,11 @@ import moment from "moment"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; +import { + MARK_JOB_AS_EXPORTED, + MARK_JOB_AS_UNINVOICED, + MARK_JOB_FOR_REEXPORT, +} from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -35,58 +40,18 @@ export function JobAdminMarkReexport({ const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); - const [markJobForReexport] = useMutation(gql` - mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - status: "${bodyshop.md_ro_statuses.default_invoiced}" - } - ) { - id - date_exported - status - date_invoiced - } - } - `); - const [markJobExported] = useMutation(gql` - mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: $date_exported - status: "${bodyshop.md_ro_statuses.default_exported}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); - const [markJobUninvoiced] = useMutation(gql` - mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - date_invoiced: null - status: "${bodyshop.md_ro_statuses.default_delivered}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); + const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT); + const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED); + const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED); const handleMarkForExport = async () => { setLoading(true); const result = await markJobForReexport({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_invoiced: bodyshop.md_ro_statuses.default_invoiced, + }, }); if (!result.errors) { @@ -108,7 +73,11 @@ export function JobAdminMarkReexport({ const handleMarkExported = async () => { setLoading(true); const result = await markJobExported({ - variables: { jobId: job.id, date_exported: moment() }, + variables: { + jobId: job.id, + date_exported: moment(), + default_exported: bodyshop.md_ro_statuses.default_exported, + }, }); await insertExportLog({ @@ -144,7 +113,10 @@ export function JobAdminMarkReexport({ const handleUninvoice = async () => { setLoading(true); const result = await markJobUninvoiced({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_delivered: bodyshop.md_ro_statuses.default_delivered, + }, }); if (!result.errors) { @@ -165,27 +137,29 @@ export function JobAdminMarkReexport({ return ( <> - - - + + + + + ); } diff --git a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx index c85404f48..f1bda15ce 100644 --- a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx +++ b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx @@ -1,9 +1,10 @@ -import { gql, useMutation } from "@apollo/client"; -import { Form, Switch, notification } from "antd"; +import { useMutation } from "@apollo/client"; +import { Switch, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; @@ -20,21 +21,11 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) { const [loading, setLoading] = useState(false); const [switchValue, setSwitchValue] = useState(job.remove_from_ar); - const [updateJob] = useMutation(gql` - mutation REMOVE_FROM_AR_JOB($jobId: uuid!, $remove_from_ar: Boolean!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { remove_from_ar: $remove_from_ar } - ) { - id - remove_from_ar - } - } - `); + const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR); const handleChange = async (value) => { setLoading(true); - const result = await updateJob({ + const result = await mutationUpdateRemoveFromAR({ variables: { jobId: job.id, remove_from_ar: value }, }); @@ -56,8 +47,19 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) { }; return ( - - - + <> +
+
+ {t("jobs.labels.remove_from_ar")}: +
+
+ +
+
+ ); } diff --git a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx index 7963fd05f..2094178c4 100644 --- a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx +++ b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx @@ -1,9 +1,10 @@ -import { gql, useMutation } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import { Button, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { UNVOID_JOB } from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -29,66 +30,17 @@ export function JobsAdminUnvoid({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [updateJob] = useMutation(gql` -mutation UNVOID_JOB($jobId: uuid!) { - update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${ - bodyshop.md_ro_statuses.default_imported - }", date_void: null}) { - id - date_void - voided - status - } - insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${ - currentUser.email - }", text: "${t("jobs.labels.unvoidnote")}"}) { - returning { - id - } - } -} - - `); - - // const result = await voidJob({ - // variables: { - // jobId: job.id, - // job: { - // status: bodyshop.md_ro_statuses.default_void, - // voided: true, - // }, - // note: [ - // { - // jobid: job.id, - // created_by: currentUser.email, - // audit: true, - // text: t("jobs.labels.voidnote", { - // date: moment().format("MM/DD/yyy"), - // time: moment().format("hh:mm a"), - // }), - // }, - // ], - // }, - // }); - - // if (!!!result.errors) { - // notification["success"]({ - // message: t("jobs.successes.voided"), - // }); - // //go back to jobs list. - // history.push(`/manage/`); - // } else { - // notification["error"]({ - // message: t("jobs.errors.voiding", { - // error: JSON.stringify(result.errors), - // }), - // }); - // } + const [mutationUnvoidJob] = useMutation(UNVOID_JOB); const handleUpdate = async (values) => { setLoading(true); - const result = await updateJob({ - variables: { jobId: job.id }, + const result = await mutationUnvoidJob({ + variables: { + jobId: job.id, + default_imported: bodyshop.md_ro_statuses.default_imported, + currentUserEmail: currentUser.email, + text: t("jobs.labels.unvoidnote"), + }, }); if (!result.errors) { @@ -110,8 +62,10 @@ mutation UNVOID_JOB($jobId: uuid!) { }; return ( - + <> + + ); } diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index adfc42765..57bb935f8 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2221,3 +2221,120 @@ export const GET_JOB_LINE_ORDERS = gql` } } `; + +export const UPDATE_REMOVE_FROM_AR = gql` + mutation UPDATE_REMOVE_FROM_AR($jobId: uuid!, $remove_from_ar: Boolean!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { remove_from_ar: $remove_from_ar } + ) { + id + remove_from_ar + } + } +`; + +export const UNVOID_JOB = gql` + mutation UNVOID_JOB( + $jobId: uuid! + $default_imported: String! + $currentUserEmail: String! + $text: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { voided: false, status: $default_imported, date_void: null } + ) { + id + date_void + voided + status + } + insert_notes( + objects: { + jobid: $jobId + audit: true + created_by: $currentUserEmail + text: $text + } + ) { + returning { + id + } + } + } +`; + +export const DELETE_INTAKE_CHECKLIST = gql` + mutation DELETE_INTAKE($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { intakechecklist: null } + ) { + id + intakechecklist + } + } +`; + +export const DELETE_DELIVERY_CHECKLIST = gql` + mutation DELETE_DELIVERY($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { deliverchecklist: null } + ) { + id + deliverchecklist + } + } +`; + +export const MARK_JOB_FOR_REEXPORT = gql` + mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!, $default_invoiced: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: null, status: $default_invoiced } + ) { + id + date_exported + status + date_invoiced + } + } +`; + +export const MARK_JOB_AS_EXPORTED = gql` + mutation MARK_JOB_AS_EXPORTED( + $jobId: uuid! + $date_exported: timestamptz! + $default_exported: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: $date_exported, status: $default_exported } + ) { + id + date_exported + date_invoiced + status + } + } +`; + +export const MARK_JOB_AS_UNINVOICED = gql` + mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, $default_delivered: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { + date_exported: null + date_invoiced: null + status: $default_delivered + } + ) { + id + date_exported + date_invoiced + status + } + } +`; From f8408908b22ea705a954cc7198b11c8db96be0b3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 21:40:05 -0500 Subject: [PATCH 049/131] - Major Progress Commit Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 131 ++++++++++++------ .../job-lifecycle/job-lifecycle.styles.scss | 0 .../jobs-detail.page.component.jsx | 17 +-- server/utils/calculateStatusDuration.js | 39 ++++-- 4 files changed, 126 insertions(+), 61 deletions(-) create mode 100644 client/src/components/job-lifecycle/job-lifecycle.styles.scss diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 58475e300..6ddbcfc51 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,27 +1,12 @@ import React, {useCallback, useEffect, useState} from 'react'; import moment from "moment"; import axios from 'axios'; -import {Card, Space, Table} from 'antd'; +import {Badge, Card, Space, Table, Tag} from 'antd'; import {gql, useQuery} from "@apollo/client"; import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; import {isEmpty} from "lodash"; -import {Bar, BarChart, CartesianGrid, Legend, Tooltip, YAxis} from "recharts"; - -const transformDataForChart = (durations) => { - const output = {}; - // output.amt = durations.total; - // output.name = 'Total'; - durations.summations.forEach((summation) => { - output[summation.status] = summation.value; - }); - return [output]; -} -const getColor = (key) => { - // Generate a random color - const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); - return randomColor; -}; +require('./job-lifecycle.styles.scss'); export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); @@ -124,34 +109,92 @@ export function JobLifecycleComponent({job, ...rest}) { {!loading ? ( lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? ( - - - - - - - - { - Object.keys(transformDataForChart(lifecycleData.durations)[0]).map((key) => { - return ( - - ) - }) - } - + + + Statuses + + + )} + style={{width: '100%'}} + > +
+ {lifecycleData.durations.summations.map((key, index, array) => { + const isFirst = index === 0; + const isLast = index === array.length - 1; + + return ( +
+ {Math.round(key.percentage)}% +
+ ); + })} +
+ +
+ {lifecycleData.durations.summations.map((key) => ( + +
+ {key.status} ({key.roundedPercentage}) +
+
+ ))} +
-
- + + Accumulated Time: {lifecycleData.durations.humanReadableTotal} + + + + + + Transitions + + + )}> +
diff --git a/client/src/components/job-lifecycle/job-lifecycle.styles.scss b/client/src/components/job-lifecycle/job-lifecycle.styles.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 0f335d982..b2e8159c8 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -289,13 +289,6 @@ export function JobsDetailPage({ form={form} /> - Lifecycle} - key="lifecycle" - > - - - Lifecycle} + key="lifecycle" + > + + + + diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index ae0ef8238..b7d432e32 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -1,17 +1,23 @@ -const moment = require('moment'); const durationToHumanReadable = require("./durationToHumanReadable"); -/** - * Calculate the duration of each status of a job - * @param transitions - * @returns {{}} - */ +const moment = require("moment"); +const _ = require("lodash"); +const crypto = require('crypto'); + +const getColor = (key) => { + const hash = crypto.createHash('sha256'); + hash.update(key); + const hashedKey = hash.digest('hex'); + const num = parseInt(hashedKey, 16); + return '#' + (num % 16777215).toString(16).padStart(6, '0'); +}; + const calculateStatusDuration = (transitions) => { let statusDuration = {}; let totalDuration = 0; let summations = []; transitions.forEach((transition, index) => { - let duration = transition.duration_minutes; + let duration = transition.duration; totalDuration += duration; if (!transition.prev_value) { @@ -42,16 +48,31 @@ const calculateStatusDuration = (transitions) => { } }); + // Calculate the percentage for each status +// Calculate the percentage for each status + let totalPercentage = 0; + const statusKeys = Object.keys(statusDuration); + statusKeys.forEach((status, index) => { + if (index !== statusKeys.length - 1) { + const percentage = (statusDuration[status].value / totalDuration) * 100; + totalPercentage += percentage; + statusDuration[status].percentage = percentage; + } else { + statusDuration[status].percentage = 100 - totalPercentage; + } + }); + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { if (status !== 'total') { - summations.push({status, value, humanReadable}); + summations.push({status, value, humanReadable, percentage: statusDuration[status].percentage, color: getColor(status), roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`}); } } const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); return { - summations, + summations: _.orderBy(summations, ['value'], ['asc']), + totalStatuses: summations.length, total: totalDuration, humanReadableTotal }; From a394d6b37e491b72e667d1077e4be52679274b60 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 25 Jan 2024 12:38:32 -0500 Subject: [PATCH 050/131] - Major Progress Commit Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 6ddbcfc51..07cb778c9 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -99,10 +99,10 @@ export function JobLifecycleComponent({job, ...rest}) { }, ]; - useEffect(() => { - console.log('LifeCycle Data'); - console.dir(lifecycleData, {depth: null}) - }, [lifecycleData]); + // useEffect(() => { + // console.log('LifeCycle Data'); + // console.dir(lifecycleData, {depth: null}) + // }, [lifecycleData]); return ( @@ -144,14 +144,12 @@ export function JobLifecycleComponent({job, ...rest}) { alignItems: 'center', margin: 0, padding: 0, + borderTop: '1px solid #f0f2f5', borderBottom: '1px solid #f0f2f5', borderLeft: isFirst ? '1px solid #f0f2f5' : undefined, borderRight: isLast ? '1px solid #f0f2f5' : undefined, - borderBottomLeftRadius: isFirst ? '5px' : undefined, - borderTopLeftRadius: isFirst ? '5px' : undefined, - borderBottomRightRadius: isLast ? '5px' : undefined, - borderTopRightRadius: isLast ? '5px' : undefined, + backgroundColor: key.color, width: `${key.percentage}%` }} From 50f84d40e1dc13d0143fd68c293b5a695bda8dfb Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 25 Jan 2024 10:30:03 -0800 Subject: [PATCH 051/131] Allow negative balance for AR. --- .../1706207204357_run_sql_migration/down.sql | 35 +++++++++++++++++++ .../1706207204357_run_sql_migration/up.sql | 33 +++++++++++++++++ .../1706207267558_run_sql_migration/down.sql | 35 +++++++++++++++++++ .../1706207267558_run_sql_migration/up.sql | 33 +++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 hasura/migrations/1706207204357_run_sql_migration/down.sql create mode 100644 hasura/migrations/1706207204357_run_sql_migration/up.sql create mode 100644 hasura/migrations/1706207267558_run_sql_migration/down.sql create mode 100644 hasura/migrations/1706207267558_run_sql_migration/up.sql diff --git a/hasura/migrations/1706207204357_run_sql_migration/down.sql b/hasura/migrations/1706207204357_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207204357_run_sql_migration/up.sql b/hasura/migrations/1706207204357_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/down.sql b/hasura/migrations/1706207267558_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/up.sql b/hasura/migrations/1706207267558_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; From 0e4f5b8b2ac1c2be6a59132ef67775aa41a8c8d9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 25 Jan 2024 13:35:20 -0500 Subject: [PATCH 052/131] - progress update. Signed-off-by: Dave Richer --- .../components/job-lifecycle/job-lifecycle.component.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 07cb778c9..1bfeef189 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -8,6 +8,13 @@ import {isEmpty} from "lodash"; require('./job-lifecycle.styles.scss'); +// Get Bodyshop record +// md_RepairStatus +// All status, array of strings, all statuses available system wide, the order is meaningful. + +// CHECK SORT OF LEGEND + +// show text on bar if text can fit export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -192,7 +199,6 @@ export function JobLifecycleComponent({job, ...rest}) { )}> -
From 908942ec09c5c492482af670b311b548b1d3c7b1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 26 Jan 2024 08:11:29 -0800 Subject: [PATCH 053/131] IO-2543 Revert Lost Sales for Datedisable --- client/src/utils/TemplateConstants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index ad614af3b..bb6c23731 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2020,7 +2020,6 @@ export const TemplateList = (type, context) => { key: "lost_sales", //idtype: "vendor", disabled: false, - datedisable: true, rangeFilter: { object: i18n.t("reportcenter.labels.objects.jobs"), field: i18n.t("jobs.fields.date_lost_sale"), From c7a0072f2dceef92e5386eb432602fb423cfbdf2 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 26 Jan 2024 11:19:03 -0500 Subject: [PATCH 054/131] - Revert Hasura Signed-off-by: Dave Richer --- hasura/metadata/tables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index b9efbb437..17ccc52a9 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4198,7 +4198,7 @@ interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook: https://worktest.home.irony.online + webhook_from_env: HASURA_API_URL headers: - name: event-secret value_from_env: EVENT_SECRET From efd1c170339fd527e729ce5a78feb91e57e3e62d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 26 Jan 2024 11:19:03 -0500 Subject: [PATCH 055/131] - Revert Hasura Signed-off-by: Dave Richer --- hasura/metadata/tables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index b9efbb437..17ccc52a9 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4198,7 +4198,7 @@ interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook: https://worktest.home.irony.online + webhook_from_env: HASURA_API_URL headers: - name: event-secret value_from_env: EVENT_SECRET From 7503d86c693889c238b3660cdeb32be238e9c9e4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 26 Jan 2024 08:42:57 -0800 Subject: [PATCH 056/131] IO-2543 Add wrap to space components to maintain limits of card --- .../jobs-admin-delete-intake.component.jsx | 2 +- .../jobs-admin-mark-reexport.component.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx index ec1bd976b..9d95f6e80 100644 --- a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx +++ b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx @@ -52,7 +52,7 @@ export default function JobAdminDeleteIntake({ job }) { return ( <> - +
+
) : ( diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index becddf9eb..7679ffeed 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1231,7 +1231,7 @@ "not_available": "N/A", "previous_status_accumulated_time": "Previous Status Accumulated Time", "title": "Job Lifecycle Component", - "title_durations": "Historical Status Duration's", + "title_durations": "Historical Status Durations", "title_loading": "Loading", "title_transitions": "Transitions" }, diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index 75b30c54c..16165d001 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -87,7 +87,6 @@ const calculateStatusDuration = (transitions, statuses) => { const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); - return { summations: _.isArray(statuses) && !_.isEmpty(statuses) ? summations.sort((a, b) => { return statuses.indexOf(a.status) - statuses.indexOf(b.status); diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js index f13e24c98..e6820c9bf 100644 --- a/server/utils/durationToHumanReadable.js +++ b/server/utils/durationToHumanReadable.js @@ -15,7 +15,7 @@ const durationToHumanReadable = (duration) => { if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); - if (!minutes && !hours && !days && !months && !years && seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + if (seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); return parts.join(', '); } From cb4a6e8774d53c9428438011c75220bff9ab05a8 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 09:05:46 -0800 Subject: [PATCH 063/131] IO-1532 resolve update logic issue for status timings. --- server/graphql-client/queries.js | 12 ++- server/job/job-status-transition.js | 135 +++++++++++++++------------- 2 files changed, 82 insertions(+), 65 deletions(-) diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 2bd83d1ab..fe1c7b6e5 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1989,12 +1989,20 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $ } }`; -exports.INSERT_NEW_TRANSITION = `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, $oldTransitionId: uuid, $duration: numeric) { +exports.INSERT_NEW_TRANSITION = ( + includeOldTransition +) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${ + includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : "" +}) { insert_transitions_one(object: $newTransition) { id } - update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { + ${ + includeOldTransition + ? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { affected_rows + }` + : "" } }`; diff --git a/server/job/job-status-transition.js b/server/job/job-status-transition.js index 960041746..d97249c06 100644 --- a/server/job/job-status-transition.js +++ b/server/job/job-status-transition.js @@ -11,82 +11,91 @@ const path = require("path"); const client = require("../graphql-client/graphql-client").client; require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); async function StatusTransition(req, res) { - const { - id: jobid, - status: value, - shopid: bodyshopid, - } = req.body.event.data.new; + const { + id: jobid, + status: value, + shopid: bodyshopid, + } = req.body.event.data.new; - // Create record OPEN on new item, enter state - // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status - // (Timeline) - // Final status is exported, there is no end date as there is no further transition (has no end date) - try { - const {update_transitions} = await client.request( - queries.UPDATE_OLD_TRANSITION, - { - jobid: jobid, - existingTransition: { - end: new Date(), - next_value: value, + // Create record OPEN on new item, enter state + // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status + // (Timeline) + // Final status is exported, there is no end date as there is no further transition (has no end date) + try { + const { update_transitions } = await client.request( + queries.UPDATE_OLD_TRANSITION, + { + jobid: jobid, + existingTransition: { + end: new Date(), + next_value: value, - //duration - }, - } - ); + //duration + }, + } + ); - let duration = - update_transitions.affected_rows === 0 - ? 0 - : new Date(update_transitions.returning[0].end) - - new Date(update_transitions.returning[0].start); + let duration = + update_transitions.affected_rows === 0 + ? 0 + : new Date(update_transitions.returning[0].end) - + new Date(update_transitions.returning[0].start); - const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { - oldTransitionId: + const resp2 = await client.request( + queries.INSERT_NEW_TRANSITION(update_transitions.affected_rows > 0), + { + ...(update_transitions.affected_rows > 0 + ? { + oldTransitionId: update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].id, - duration, - newTransition: { - bodyshopid: bodyshopid, - jobid: jobid, - start: - update_transitions.affected_rows === 0 - ? new Date() - : update_transitions.returning[0].end, - prev_value: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].value, - value: value, - type: "status", - }, - }); + ? null + : update_transitions.returning[0].id, + duration, + } + : {}), + newTransition: { + bodyshopid: bodyshopid, + jobid: jobid, + start: + update_transitions.affected_rows === 0 + ? new Date() + : update_transitions.returning[0].end, + prev_value: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].value, + value: value, + type: "status", + }, + } + ); - //Check to see if there is an existing status transition record. - //Query using Job ID, start is not null, end is null. + logger.log("job-transition-update-result", "DEBUG", null, jobid, resp2); - //If there is no existing record, this is the start of the transition life cycle. - // Create the initial transition record. + //Check to see if there is an existing status transition record. + //Query using Job ID, start is not null, end is null. - //If there is a current status transition record, update it with the end date, duration, and next value. + //If there is no existing record, this is the start of the transition life cycle. + // Create the initial transition record. - res.sendStatus(200); //.json(ret); - } catch (error) { - logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { - message: error.message, - stack: error.stack, - }); + //If there is a current status transition record, update it with the end date, duration, and next value. - res.status(400).send(JSON.stringify(error)); - } + res.sendStatus(200); //.json(ret); + } catch (error) { + logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { + message: error.message, + stack: error.stack, + }); + + res.status(400).send(JSON.stringify(error)); + } } exports.statustransition = StatusTransition; From 8bc1a9d9ee87403db3e9d85014e8e2dea738db89 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 12:47:12 -0800 Subject: [PATCH 064/131] Initial sentry improvements to deploy and verify against test. --- client/.env.production | 2 +- client/.gitignore | 3 + client/craco.config.js | 30 +- client/package-lock.json | 636 ++++++++++++------ client/package.json | 9 +- client/src/App/App.container.jsx | 5 +- .../error-boundary.component.jsx | 9 +- .../jobs-list/jobs-list.component.jsx | 8 + client/src/graphql/apollo-error-handling.js | 12 +- client/src/index.js | 47 +- client/src/utils/GraphQLClient.js | 42 +- 11 files changed, 533 insertions(+), 270 deletions(-) create mode 100644 client/.gitignore diff --git a/client/.env.production b/client/.env.production index a4cdecee5..cdf721dcb 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,4 +1,4 @@ -GENERATE_SOURCEMAP=false +GENERATE_SOURCEMAP=true REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql REACT_APP_GA_CODE=231103507 diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 000000000..387b4257c --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,3 @@ + +# Sentry Config File +.sentryclirc diff --git a/client/craco.config.js b/client/craco.config.js index ac2174e3f..66cddad73 100644 --- a/client/craco.config.js +++ b/client/craco.config.js @@ -1,25 +1,25 @@ // craco.config.js const TerserPlugin = require("terser-webpack-plugin"); const CracoLessPlugin = require("craco-less"); -const SentryWebpackPlugin = require("@sentry/webpack-plugin"); +//const SentryWebpackPlugin = require("@sentry/webpack-plugin"); module.exports = { plugins: [ - { - plugin: SentryWebpackPlugin, - options: { - // sentry-cli configuration - authToken: - "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", - org: "snapt-software", - project: "imexonline", - release: process.env.REACT_APP_GIT_SHA, + // { + // plugin: SentryWebpackPlugin, + // options: { + // // sentry-cli configuration + // authToken: + // "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", + // org: "snapt-software", + // project: "imexonline", + // release: process.env.REACT_APP_GIT_SHA, - // webpack-specific configuration - include: ".", - ignore: ["node_modules", "webpack.config.js"], - }, - }, + // // webpack-specific configuration + // include: ".", + // ignore: ["node_modules", "webpack.config.js"], + // }, + // }, { plugin: CracoLessPlugin, options: { diff --git a/client/package-lock.json b/client/package-lock.json index 91b98ad66..25a526f60 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,12 +13,14 @@ "@craco/craco": "^7.0.0", "@fingerprintjs/fingerprintjs": "^3.4.2", "@jsreport/browser-client": "^3.1.0", - "@sentry/react": "^7.40.0", + "@sentry/cli": "^2.27.0", + "@sentry/react": "^7.99.0", "@sentry/tracing": "^7.40.0", "@splitsoftware/splitio-react": "^1.8.1", "@tanem/react-nprogress": "^5.0.8", "antd": "^4.24.8", "apollo-link-logger": "^2.0.1", + "apollo-link-sentry": "^3.3.0", "axios": "^1.3.4", "craco-less": "^2.0.0", "dinero.js": "^1.9.1", @@ -4214,40 +4216,195 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "license": "MIT" }, - "node_modules/@sentry/browser": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.40.0.tgz", - "integrity": "sha512-07rZ+cTcpmYB1r84/oZtmSPJJvLCxW8yIh/5s4MdKRyZpqIDKhOz6cCS/4j+l1V+MeLcNLZBjFtNdKA2eocTpg==", - "license": "MIT", + "node_modules/@sentry-internal/feedback": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.99.0.tgz", + "integrity": "sha512-exIO1o+bE0MW4z30FxC0cYzJ4ZHSMlDPMHCBDPzU+MWGQc/fb8s58QUrx5Dnm6HTh9G3H+YlroCxIo9u0GSwGQ==", "dependencies": { - "@sentry/core": "7.40.0", - "@sentry/replay": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "tslib": "^1.9.3" + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" }, "engines": { "node": ">=8" } }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@sentry-internal/feedback/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.99.0.tgz", + "integrity": "sha512-PoIkfusToDq0snfl2M6HJx/1KJYtXxYhQplrn11kYadO04SdG0XGXf4h7wBTMEQ7LDEAtQyvsOu4nEQtTO3YjQ==", + "dependencies": { + "@sentry/core": "7.99.0", + "@sentry/replay": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.99.0.tgz", + "integrity": "sha512-z3JQhHjoM1KdM20qrHwRClKJrNLr2CcKtCluq7xevLtXHJWNAQQbafnWD+Aoj85EWXBzKt9yJMv2ltcXJ+at+w==", + "dependencies": { + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.99.0.tgz", + "integrity": "sha512-bgfoUv3wkwwLgN5YUOe0ibB3y268ZCnamZh6nLFqnY/UBKC1+FXWFdvzVON/XKUm62LF8wlpCybOf08ebNj2yg==", + "dependencies": { + "@sentry-internal/feedback": "7.99.0", + "@sentry-internal/replay-canvas": "7.99.0", + "@sentry-internal/tracing": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/replay": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@sentry/cli": { - "version": "1.74.6", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.74.6.tgz", - "integrity": "sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==", - "dev": true, + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.27.0.tgz", + "integrity": "sha512-pc0opd71W8lGhYvmB1keQtJkarxzCS9f9ErKYv6TfXOOX6drvwkyA6vD/6xEnpzyvqGAuGRU4T4sEeLD3irwUQ==", "hasInstallScript": true, - "license": "BSD-3-Clause", "dependencies": { "https-proxy-agent": "^5.0.0", - "mkdirp": "^0.5.5", "node-fetch": "^2.6.7", - "npmlog": "^4.1.2", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" @@ -4256,7 +4413,124 @@ "sentry-cli": "bin/sentry-cli" }, "engines": { - "node": ">= 8" + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.27.0", + "@sentry/cli-linux-arm": "2.27.0", + "@sentry/cli-linux-arm64": "2.27.0", + "@sentry/cli-linux-i686": "2.27.0", + "@sentry/cli-linux-x64": "2.27.0", + "@sentry/cli-win32-i686": "2.27.0", + "@sentry/cli-win32-x64": "2.27.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.27.0.tgz", + "integrity": "sha512-/DOZlN5rK19g7YP2OaVNauQhUrRfJ88RDr6qURFiqdxYHDc3isPFGHZJmeZBTwOnDDepyZb4XLaOyfwvAOxHig==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.27.0.tgz", + "integrity": "sha512-JmMQ9zgFhkZUEN5WIYuJisu4Jif/ThRHDjbsbXBRbUkkgRn88hgUfg299djMvlZZxjpl3K9AEua+1TIUeQd0Sg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.27.0.tgz", + "integrity": "sha512-f+zuB9XGfB8pNamNgSDhqsavuLuzi6saZxbr3uQf30bA5AESI5hspOd1zPcidOORCVZxiPzQe3+T7avBI1XLuw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.27.0.tgz", + "integrity": "sha512-/4eyz7jnYp20mZqNtpvCEBkxFW0nEjEZRo2BiASQ5/7K8CmoJRe1vhpDA0WOfzi1zTFIfpdE1/RZm2CjHS6DHQ==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.27.0.tgz", + "integrity": "sha512-ptu7wXecnYssihzHlxEOaqbFHWmNEfbepBKGXTdWK2kC+D51+7yHsR9xRdThwVID1bisFgjAveKmBQjmKuXjHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.27.0.tgz", + "integrity": "sha512-Db4/xmdE5qV4Aq7Yc8vRw22Y46JJdGMdsMsl5jIf0GVSQPgO23O/2uTiDGpPOdeq91K9EtvpH1zQfDLIfLMaXw==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.27.0.tgz", + "integrity": "sha512-q7y/BH4iGfs0TD5PXh2Q8oqnTbOIufoT1NWJcKqvZcOiqCLK3PNUiq7xUeX1PMTrFYAh3Bm6EekOnMavqvbGmg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, "node_modules/@sentry/core": { @@ -4280,16 +4554,15 @@ "license": "0BSD" }, "node_modules/@sentry/react": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.40.0.tgz", - "integrity": "sha512-7yYagpOCdsXnVTtLL8Y7wAf2xXgsk2ncuju3O/G4kEckkLewZWmQeoknOSGFlAgVdGNhTaXc2WGzgOiBMOkhug==", - "license": "MIT", + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.99.0.tgz", + "integrity": "sha512-RtHwgzMHJhzJfSQpVG0SDPQYMTGDX3Q37/YWI59S4ALMbSW4/F6n/eQAvGVYZKbh2UCSqgFuRWaXOYkSZT17wA==", "dependencies": { - "@sentry/browser": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" + "@sentry/browser": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0", + "hoist-non-react-statics": "^3.3.2" }, "engines": { "node": ">=8" @@ -4298,26 +4571,82 @@ "react": "15.x || 16.x || 17.x || 18.x" } }, - "node_modules/@sentry/react/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@sentry/react/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@sentry/replay": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.40.0.tgz", - "integrity": "sha512-Y9Kvo9jKouUdrHQhHVv5SmWZClF5o7BFI6oVpLlv4zXORPQlyoZONM/9sxiMvvH73alDSpxzCoxyhlypAOH4ww==", - "license": "MIT", + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.99.0.tgz", + "integrity": "sha512-gyN/I2WpQrLAZDT+rScB/0jnFL2knEVBo8U8/OVt8gNP20Pq8T/rDZKO/TG0cBfvULDUbJj2P4CJryn2p/O2rA==", "dependencies": { - "@sentry/core": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0" + "@sentry-internal/tracing": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" }, "engines": { "node": ">=12" } }, + "node_modules/@sentry/replay/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sentry/tracing": { "version": "7.40.0", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.40.0.tgz", @@ -4381,6 +4710,27 @@ "node": ">= 8" } }, + "node_modules/@sentry/webpack-plugin/node_modules/@sentry/cli": { + "version": "1.77.3", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.77.3.tgz", + "integrity": "sha512-c3eDqcDRmy4TFz2bFU5Y6QatlpoBPPa8cxBooaS4aMQpnIdLYPF1xhyyiW0LQlDUNc3rRjNF7oN5qKoaRoMTQQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "https-proxy-agent": "^5.0.0", + "mkdirp": "^0.5.5", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -6112,12 +6462,21 @@ "@apollo/client": "^3.0.0" } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "license": "ISC" + "node_modules/apollo-link-sentry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-3.3.0.tgz", + "integrity": "sha512-wLffWmo5sRw3rHN1Ck6azM0oxObvtaBBf3AC8cLX4SxhyjmkRIagGDji6CFkyAhxupPz0b9/H1u4Ocx+63lNug==", + "dependencies": { + "deepmerge": "^4.2.2", + "dot-prop": "^6.0.0", + "tslib": "^2.0.3", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "@apollo/client": "^3.2.3", + "@sentry/browser": "^7.41.0", + "graphql": "15 - 16" + } }, "node_modules/arch": { "version": "2.2.0", @@ -6140,17 +6499,6 @@ ], "license": "MIT" }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -7571,16 +7919,6 @@ "node": ">=4" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -7743,13 +8081,6 @@ "node": ">=0.8" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -8911,13 +9242,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/denque": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", @@ -9218,6 +9542,28 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", @@ -10464,6 +10810,8 @@ }, "node_modules/eventemitter2": { "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true, "license": "MIT" }, @@ -11283,74 +11631,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11753,13 +12033,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -15744,19 +16017,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -15769,16 +16029,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", @@ -17794,7 +18044,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -20526,13 +20775,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -23067,16 +23309,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index 602f30e12..a93f91fac 100644 --- a/client/package.json +++ b/client/package.json @@ -9,12 +9,14 @@ "@craco/craco": "^7.0.0", "@fingerprintjs/fingerprintjs": "^3.4.2", "@jsreport/browser-client": "^3.1.0", - "@sentry/react": "^7.40.0", + "@sentry/cli": "^2.27.0", + "@sentry/react": "^7.99.0", "@sentry/tracing": "^7.40.0", "@splitsoftware/splitio-react": "^1.8.1", "@tanem/react-nprogress": "^5.0.8", "antd": "^4.24.8", "apollo-link-logger": "^2.0.1", + "apollo-link-sentry": "^3.3.0", "axios": "^1.3.4", "craco-less": "^2.0.0", "dinero.js": "^1.9.1", @@ -88,13 +90,14 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "craco start", - "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build", + "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps", "build:test": "env-cmd -f .env.test npm run build", "build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'", "buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build", "test": "cypress open", "eject": "react-scripts eject", - "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ." + "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .", + "sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build" }, "eslintConfig": { "extends": [ diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index 74f6ba4df..ffbe34c10 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import client from "../utils/GraphQLClient"; import App from "./App"; +import * as Sentry from "@sentry/react"; moment.locale("en-US"); @@ -18,7 +19,7 @@ export const factory = SplitSdk({ }, }); -export default function AppContainer() { +function AppContainer() { const { t } = useTranslation(); return ( @@ -42,3 +43,5 @@ export default function AppContainer() { ); } + +export default Sentry.withProfiler(AppContainer); diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index babae0b9e..89a14e2a8 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -2,7 +2,7 @@ import { Button, Col, Collapse, Result, Row, Space } from "antd"; import React from "react"; import { withTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; - +import * as Sentry from "@sentry/react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { @@ -138,7 +138,6 @@ class ErrorBoundary extends React.Component { } } -export default connect( - mapStateToProps, - mapDispatchToProps -)(withTranslation()(ErrorBoundary)); +export default Sentry.withErrorBoundary( + connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary)) +); diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 7761c0ed9..fd3d0762e 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -370,6 +370,14 @@ export function JobsList({ bodyshop }) { } > +
{ - if (graphQLErrors) - graphQLErrors.forEach(({ message, locations, path }) => + if (graphQLErrors) { + graphQLErrors.forEach(({ message, locations, path }) => { console.log( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` - ) - ); + ); + Sentry.captureException({ message, locations, path }); + }); + } if (networkError) console.log(`[Network error]: ${JSON.stringify(networkError)}`); console.log(operation.getContext()); + return forward(operation); } ); diff --git a/client/src/index.js b/client/src/index.js index 2006eccee..155356b24 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -14,38 +14,33 @@ import { persistor, store } from "./redux/store"; import reportWebVitals from "./reportWebVitals"; import "./translations/i18n"; import "./utils/CleanAxios"; + //import { BrowserTracing } from "@sentry/tracing"; // Dinero.defaultCurrency = "CAD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; -if (process.env.NODE_ENV !== "development") { - Sentry.init({ - dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027", - ignoreErrors: [ - "ResizeObserver loop", - "Module specifier, 'fs' does not start", - "Module specifier, 'zlib' does not start with", - ], - integrations: [ - // new BrowserTracing(), - // new Sentry.Integrations.Breadcrumbs({ console: true }), - // new Sentry.Replay(), - ], - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - // replaysSessionSampleRate: 0.1, - // // If the entire session is not sampled, use the below sample rate to sample - // // sessions when an error occurs. - // replaysOnErrorSampleRate: 1.0, - environment: process.env.NODE_ENV, - // tracesSampleRate: 0.2, - // We recommend adjusting this value in production, or using tracesSampler - // for finer control - // tracesSampleRate: 0.5, - }); -} +//if (process.env.NODE_ENV !== "development") { +Sentry.init({ + dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027", + ignoreErrors: [ + "ResizeObserver loop", + "Module specifier, 'fs' does not start", + "Module specifier, 'zlib' does not start with", + ], + integrations: [ + Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: true, + }), + new Sentry.BrowserTracing(), + ], + tracesSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + environment: process.env.NODE_ENV, +}); +//} ReactDOM.render( diff --git a/client/src/utils/GraphQLClient.js b/client/src/utils/GraphQLClient.js index b6c2c0091..f3758e2e4 100644 --- a/client/src/utils/GraphQLClient.js +++ b/client/src/utils/GraphQLClient.js @@ -12,6 +12,8 @@ import apolloLogger from "apollo-link-logger"; //import axios from "axios"; import { auth } from "../firebase/firebase.utils"; import errorLink from "../graphql/apollo-error-handling"; +import { SentryLink } from "apollo-link-sentry"; + //import { store } from "../redux/store"; const httpLink = new HttpLink({ uri: process.env.REACT_APP_GRAPHQL_ENDPOINT, @@ -105,18 +107,30 @@ const link = split( const authLink = setContext((_, { headers }) => { return ( auth.currentUser && - auth.currentUser.getIdToken().then((token) => { - if (token) { - return { - headers: { - ...headers, - authorization: token ? `Bearer ${token}` : "", - }, - }; - } else { + auth.currentUser + .getIdToken() + .then((token) => { + if (token) { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; + } else { + console.error( + "Authentication error. Unable to add authorization token because it was empty." + ); + return { headers }; + } + }) + .catch((error) => { + console.error( + "Authentication error. Unable to add authorization token.", + error.message + ); return { headers }; - } - }) + }) ); }); @@ -138,8 +152,10 @@ if (process.env.NODE_ENV === "development") { } middlewares.push( - roundTripLink.concat( - retryLink.concat(errorLink.concat(authLink.concat(link))) + new SentryLink().concat( + roundTripLink.concat( + retryLink.concat(errorLink.concat(authLink.concat(link))) + ) ) ); From 2427bc72f204b40e9bc586c9bf86634815f0467d Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 12:53:07 -0800 Subject: [PATCH 065/131] Updated CI. --- .circleci/config.yml | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e047be49..49ef979fb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,21 +48,11 @@ jobs: steps: - checkout: path: ~/repo - - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - run: name: Install Dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn + command: npm i - - run: yarn run build + - run: npm run build - aws-s3/sync: from: build @@ -98,21 +88,11 @@ jobs: steps: - checkout: path: ~/repo - - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - run: name: Install Dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn + command: npm i - - run: yarn run build:test + - run: npm run build:test - aws-s3/sync: from: build From b706b96d32341651b7513126008b6a5fcb32941f Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 13:14:27 -0800 Subject: [PATCH 066/131] Remove sentry test button. --- .../components/jobs-list/jobs-list.component.jsx | 16 ++++------------ .../production-board-kanban.component.jsx | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index fd3d0762e..68b3f3425 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -1,8 +1,8 @@ import { - BranchesOutlined, - ExclamationCircleFilled, - PauseCircleOutlined, - SyncOutlined, + BranchesOutlined, + ExclamationCircleFilled, + PauseCircleOutlined, + SyncOutlined, } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd"; @@ -370,14 +370,6 @@ export function JobsList({ bodyshop }) { } > -
New Card is somewhere in the middle"); movedCardNewKanbanParent = newChildCard.kanbanparent; } else { - throw new Error("==> !!!!!!Couldn't find a parent.!!!! <=="); + console.log("==> !!!!!!Couldn't find a parent.!!!! <=="); } const newChildCardNewParent = newChildCard ? card.id : null; const update = await client.mutate({ @@ -153,7 +153,7 @@ export function ProductionBoardKanbanComponent({ 0 ) .toFixed(1); - const totalLAB = data + const totalLAB = data .reduce( (acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0 From ce6940629d2d4275ff57f1223b20dc06cc64d630 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 13:25:48 -0800 Subject: [PATCH 067/131] Exclude source map upload in CI. --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 49ef979fb..fdea2df0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,6 +57,7 @@ jobs: - aws-s3/sync: from: build to: "s3://imex-online-production/" + arguments: "--exlcude '*.map'" - jira/notify test-hasura-migrate: @@ -97,6 +98,7 @@ jobs: - aws-s3/sync: from: build to: "s3://imex-online-test/" + arguments: "--exlcude '*.map'" - jira/notify admin-app-build: From 7daf7540b355be653f75cf0e7030056c74915171 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 13:32:58 -0800 Subject: [PATCH 068/131] Resolve Typo in CI. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fdea2df0d..437f570d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,7 +57,7 @@ jobs: - aws-s3/sync: from: build to: "s3://imex-online-production/" - arguments: "--exlcude '*.map'" + arguments: "--exclude '*.map'" - jira/notify test-hasura-migrate: @@ -98,7 +98,7 @@ jobs: - aws-s3/sync: from: build to: "s3://imex-online-test/" - arguments: "--exlcude '*.map'" + arguments: "--exclude '*.map'" - jira/notify admin-app-build: From da1ddb874f64cb21da109dc01be458bd2eaaa5d1 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 30 Jan 2024 16:58:11 -0800 Subject: [PATCH 069/131] Change tracing targets. --- client/src/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/index.js b/client/src/index.js index 155356b24..c94644d3b 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -34,7 +34,12 @@ Sentry.init({ maskAllText: false, blockAllMedia: true, }), - new Sentry.BrowserTracing(), + new Sentry.BrowserTracing({}), + ], + tracePropagationTargets: [ + "api.imex.online", + "api.test.imex.online", + "db.imex.online", ], tracesSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, From a74a9ba5a16198e322e506b977ff1d62e6a6e1c2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 31 Jan 2024 09:59:56 -0800 Subject: [PATCH 070/131] IO-2624 federal_tax_exempt destructure --- .../components/bill-enter-modal/bill-enter-modal.container.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index 617b9e603..c106a8c4c 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -94,6 +94,7 @@ function BillEnterModalContainer({ location, outstanding_returns, inventory, + federal_tax_exempt, ...remainingValues } = values; From 830d2c87d24fa1a63314f18adf72434670178255 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 2 Feb 2024 11:55:57 -0800 Subject: [PATCH 071/131] IO-2626 CSI Pages Move to Server side initial commit --- .../csi-response-list-paginated.component.jsx | 5 +- .../shop-csi-config-form.component.jsx | 4 +- .../shop-csi-config.component.jsx | 6 +- client/src/pages/csi/csi.container.page.jsx | 329 +++++++++++------- client/src/translations/en_us/common.json | 12 +- server.js | 1 + server/csi/csi.js | 2 + server/csi/lookup.js | 24 ++ server/csi/submit.js | 29 ++ server/graphql-client/queries.js | 20 ++ server/routes/csiRoutes.js | 8 + 11 files changed, 293 insertions(+), 147 deletions(-) create mode 100644 server/csi/csi.js create mode 100644 server/csi/lookup.js create mode 100644 server/csi/submit.js create mode 100644 server/routes/csiRoutes.js diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index f73e35512..c8bdb2ba5 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -5,9 +5,9 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useHistory, useLocation } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; +import { pageLimit } from "../../utils/config"; import { alphaSort } from "../../utils/sorters"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; export default function CsiResponseListPaginated({ refetch, @@ -29,7 +29,6 @@ export default function CsiResponseListPaginated({ title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - width: "8%", sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sortOrder: sortcolumn === "ro_number" && sortorder, @@ -45,7 +44,6 @@ export default function CsiResponseListPaginated({ key: "owner", ellipsis: true, sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), - width: "25%", sortOrder: sortcolumn === "owner" && sortorder, render: (text, record) => { return record.job.owner ? ( @@ -65,7 +63,6 @@ export default function CsiResponseListPaginated({ key: "completedon", ellipsis: true, sorter: (a, b) => a.completedon - b.completedon, - width: "25%", sortOrder: sortcolumn === "completedon" && sortorder, render: (text, record) => { return record.completedon ? ( diff --git a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx index 4ed0c6033..9818bf896 100644 --- a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx +++ b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx @@ -1,5 +1,5 @@ -import React from "react"; import { Form } from "antd"; +import React from "react"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; export default function ShopCsiConfigForm({ selectedCsi }) { @@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) { return (
- The Config Form {readOnly} + {readOnly} {selectedCsi && ( ; return (
- The Config Form
- + + diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index 4076a01a4..7295fbb94 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -1,88 +1,62 @@ -import { useQuery, useMutation } from "@apollo/client"; -import { Form, Layout, Typography, Button, Result } from "antd"; -import React, { useState } from "react"; +// import { useMutation, useQuery } from "@apollo/client"; +import { Button, Form, Layout, Result, Typography } from "antd"; +import axios from "axios"; +import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; import { useParams } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; import AlertComponent from "../../components/alert/alert.component"; import ConfigFormComponents from "../../components/config-form-components/config-form-components.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; import { selectCurrentUser } from "../../redux/user/user.selectors"; +import { DateTimeFormat } from "./../../utils/DateFormatter"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); +const mapDispatchToProps = (dispatch) => ({}); + export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage); export function CsiContainerPage({ currentUser }) { const { surveyId } = useParams(); const [form] = Form.useForm(); + const [axiosResponse, setAxiosResponse] = useState(null); const [submitting, setSubmitting] = useState({ loading: false, submitted: false, }); - - const { loading, error, data } = useQuery(QUERY_SURVEY, { - variables: { surveyId }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); - const { t } = useTranslation(); - const [completeSurvey] = useMutation(COMPLETE_SURVEY); - if (loading) return ; - if (error || !!!data.csi_by_pk) - return ( -
- - {error ? ( -
ERROR: {error.graphQLErrors.map((e) => e.message)}
- ) : null} -
-
- ); - - const handleFinish = async (values) => { - setSubmitting({ ...submitting, loading: true }); - - const result = await completeSurvey({ - variables: { - surveyId, - survey: { - response: values, - valid: false, - completedon: new Date(), - }, - }, - }); - - if (!!!result.errors) { - setSubmitting({ ...submitting, loading: false, submitted: true }); - } else { - setSubmitting({ - ...submitting, + const getAxiosData = useCallback(async () => { + try { + setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true })); + const response = await axios.post("/csi/lookup", { surveyId }); + setSubmitting((prevSubmitting) => ({ + ...prevSubmitting, loading: false, - error: JSON.stringify(result.errors), + })); + setAxiosResponse(response.data); + } catch (error) { + console.error(`Something went wrong...: ${error.message}`); + console.dir({ + stack: error?.stack, + message: error?.message, }); } - }; + }, [setAxiosResponse, surveyId]); - const { - relateddata: { bodyshop, job }, - csiquestion: { config: csiquestions }, - } = data.csi_by_pk; + useEffect(() => { + getAxiosData().catch((err) => + console.error( + `Something went wrong fetching axios data: ${err.message || ""}` + ) + ); + }, [getAxiosData]); - if (currentUser && currentUser.authorized) + // Return if authorized + if (currentUser && currentUser.authorized) { return ( ); + } - return ( - -
-
- {bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( - Logo - ) : null} -
- {bodyshop.shopname || ""} -
{`${bodyshop.address1 || ""}`}
-
{`${bodyshop.address2 || ""}`}
-
{`${bodyshop.city || ""} ${bodyshop.state || ""} ${ - bodyshop.zip_post || "" - }`}
+ if (submitting.loading) return ; + + const handleFinish = async (values) => { + try { + setSubmitting({ ...submitting, loading: true, submitting: true }); + const result = await axios.post("/csi/submit", { surveyId, values }); + console.log("result", result); + if (!!!result.errors && result.data.update_csi.affected_rows > 0) { + setSubmitting({ ...submitting, loading: false, submitted: true }); + } + } catch (error) { + console.error(`Something went wrong...: ${error.message}`); + console.dir({ + stack: error?.stack, + message: error?.message, + }); + } + }; + + if (!axiosResponse || axiosResponse.csi_by_pk === null) { + // Do something here , this is where you would return a loading box or something + return ( + <> + + + + + + + + {`Copyright ImEX.Online. Survey ID: ${surveyId}`} + + + + ); + } else { + const { + relateddata: { bodyshop, job }, + csiquestion: { config: csiquestions }, + } = axiosResponse.csi_by_pk; + + return ( + +
+
+ {bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( + Logo + ) : null} +
+ + {bodyshop.shopname || ""} + + + {`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${ + bodyshop.address2 || "" + }`.trim()} + + + {`${bodyshop.city || ""}${ + bodyshop.city && bodyshop.state ? ", " : "" + }${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()} + +
+ {t("csi.labels.title")} + + {t("csi.labels.greeting", { + name: job.ownr_co_nm || job.ownr_fn || "", + })} + + + {t("csi.labels.intro", { shopname: bodyshop.shopname || "" })} +
- {t("csi.labels.title")} - {`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`} - - {`At ${ - bodyshop.shopname || "" - }, we value your feedback. We would love to - hear what you have to say. Please fill out the form below.`} - -
- {submitting.error ? ( - - ) : null} + {submitting.error ? ( + + ) : null} - {submitting.submitted ? ( - - - - ) : ( - -
- - - -
- )} - - - {`Copyright ImEX.Online. Survey ID: ${surveyId}`} - - - ); + {submitting.submitted ? ( + + + + ) : ( + +
+ {axiosResponse.csi_by_pk.valid ? ( + <> + + + + ) : ( + <> + + + {t("csi.successes.submittedsub")} + + + )} + +
+ )} + + {t("csi.labels.copyright")}{" "} + {t("csi.fields.surveyid", { surveyId: surveyId })} + + + ); + } } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 7679ffeed..c73b84bef 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -838,17 +838,23 @@ "creating": "Error creating survey {{message}}", "notconfigured": "You do not have any current CSI Question Sets configured.", "notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.", - "notfoundtitle": "No survey found." + "notfoundtitle": "No survey found.", + "surveycompletetitle": "Survey previously completed", + "surveycompletesubtitle": "This survey was already completed on {{date}}." }, "fields": { "completedon": "Completed On", - "created_at": "Created At" + "created_at": "Created At", + "surveyid": "Survey ID {{surveyId}}" }, "labels": { "nologgedinuser": "Please log out of ImEX Online", "nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.", "noneselected": "No response selected.", - "title": "Customer Satisfaction Survey" + "title": "Customer Satisfaction Survey", + "greeting": "Hi {{name}}!", + "intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.", + "copyright": "Copyright © $t(titles.app). All Rights Reserved." }, "successes": { "created": "CSI created successfully. ", diff --git a/server.js b/server.js index 1eaa4b8ec..dbb0d0a5e 100644 --- a/server.js +++ b/server.js @@ -74,6 +74,7 @@ app.use('/adm', require("./server/routes/adminRoutes")); app.use('/tech', require("./server/routes/techRoutes")); app.use('/intellipay', require("./server/routes/intellipayRoutes")); app.use('/cdk', require("./server/routes/cdkRoutes")); +app.use('/csi', require("./server/routes/csiRoutes")); // Default route for forbidden access app.get("/", (req, res) => { diff --git a/server/csi/csi.js b/server/csi/csi.js new file mode 100644 index 000000000..819a9ebc7 --- /dev/null +++ b/server/csi/csi.js @@ -0,0 +1,2 @@ +exports.lookup = require("./lookup").default; +exports.submit = require("./submit").default; \ No newline at end of file diff --git a/server/csi/lookup.js b/server/csi/lookup.js new file mode 100644 index 000000000..48154cdb8 --- /dev/null +++ b/server/csi/lookup.js @@ -0,0 +1,24 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const logger = require("../utils/logger"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + +const client = require("../graphql-client/graphql-client").client; + +exports.default = async (req, res) => { + try { + logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null); + const response = await client.request(queries.QUERY_SURVEY, { + surveyId: req.body.surveyId, + }); + res.status(200).json(response); + } catch (error) { + logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error); + res.status(400).json(error); + } +}; diff --git a/server/csi/submit.js b/server/csi/submit.js new file mode 100644 index 000000000..da5727531 --- /dev/null +++ b/server/csi/submit.js @@ -0,0 +1,29 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const logger = require("../utils/logger"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + +const client = require("../graphql-client/graphql-client").client; + +exports.default = async (req, res) => { + try { + logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null); + const response = await client.request(queries.COMPLETE_SURVEY, { + surveyId: req.body.surveyId, + survey: { + response: req.body.values, + valid: false, + completedon: new Date(), + }, + }); + res.status(200).json(response); + } catch (error) { + logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error); + res.status(400).json(error); + } +}; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index fe1c7b6e5..b2fd4e23e 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2156,3 +2156,23 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { shopid } }`; + +exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) { + csi_by_pk(id: $surveyId) { + completedon + csiquestion { + id + config + } + id + relateddata + valid + validuntil + } +}`; + +exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) { + update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) { + affected_rows + } + }`; \ No newline at end of file diff --git a/server/routes/csiRoutes.js b/server/routes/csiRoutes.js new file mode 100644 index 000000000..8f47a2b2d --- /dev/null +++ b/server/routes/csiRoutes.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const { lookup, submit } = require("../csi/csi"); + +router.post("/lookup", lookup); +router.post("/submit", submit); + +module.exports = router; From 0d1ff6390cdb88d9d76d5196a88bac98391d809b Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 2 Feb 2024 12:35:18 -0800 Subject: [PATCH 072/131] IO-2626 Correct Error Page footer --- client/src/pages/csi/csi.container.page.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index 7295fbb94..96b9663a9 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -115,7 +115,8 @@ export function CsiContainerPage({ currentUser }) { - {`Copyright ImEX.Online. Survey ID: ${surveyId}`} + {t("csi.labels.copyright")}{" "} + {t("csi.fields.surveyid", { surveyId: surveyId })} From 97a1bd66d166c354b7b07f472a67b98ec9b38e3e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 2 Feb 2024 22:45:06 -0800 Subject: [PATCH 073/131] IO-2626 Correct Sorting, Linking, Pagination and Update Response Container --- .../csi-response-form.container.jsx | 23 ++++-- .../csi-response-list-paginated.component.jsx | 70 +++++++++---------- client/src/graphql/csi.queries.js | 11 ++- .../shop-csi/shop-csi.container.page.jsx | 38 +++------- client/src/translations/en_us/common.json | 3 +- 5 files changed, 67 insertions(+), 78 deletions(-) diff --git a/client/src/components/csi-response-form/csi-response-form.container.jsx b/client/src/components/csi-response-form/csi-response-form.container.jsx index a1882b09a..38b06744d 100644 --- a/client/src/components/csi-response-form/csi-response-form.container.jsx +++ b/client/src/components/csi-response-form/csi-response-form.container.jsx @@ -1,19 +1,19 @@ import { useQuery } from "@apollo/client"; import { Card, Form, Result } from "antd"; -import queryString from "query-string"; +// import queryString from "query-string"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; +// import { useLocation } from "react-router-dom"; import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; +import { DateFormatter } from "../../utils/DateFormatter"; import AlertComponent from "../alert/alert.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; -export default function CsiResponseFormContainer() { +export default function CsiResponseFormContainer({ responseid }) { const { t } = useTranslation(); const [form] = Form.useForm(); - const searchParams = queryString.parse(useLocation().search); - const { responseid } = searchParams; + const { loading, error, data } = useQuery(QUERY_CSI_RESPONSE_BY_PK, { variables: { id: responseid, @@ -44,6 +44,19 @@ export default function CsiResponseFormContainer() { readOnly componentList={data.csi_by_pk.csiquestion.config} /> + {data.csi_by_pk.completedon ? ( + <> + {t("csi.fields.completedon")} + {": "} + {data.csi_by_pk.completedon} + + ) : data.csi_by_pk.validuntil ? ( + <> + {t("csi.fields.validuntil")} + {": "} + {data.csi_by_pk.validuntil} + + ) : null} ); diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index c8bdb2ba5..b41011f07 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -1,37 +1,37 @@ import { SyncOutlined } from "@ant-design/icons"; import { Button, Card, Table } from "antd"; -import queryString from "query-string"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; import { pageLimit } from "../../utils/config"; -import { alphaSort } from "../../utils/sorters"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import { alphaSort, dateSort } from "../../utils/sorters"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; export default function CsiResponseListPaginated({ refetch, loading, responses, total, + setresponseid, }) { - const search = queryString.parse(useLocation().search); - const { responseid, page, sortcolumn, sortorder } = search; - const history = useHistory(); const [state, setState] = useState({ sortedInfo: {}, filteredInfo: { text: "" }, + page: "", }); - const { t } = useTranslation(); + const columns = [ { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), - sortOrder: sortcolumn === "ro_number" && sortorder, - + sorter: (a, b) => alphaSort(a.job?.ro_number, b.job?.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( {record.job.ro_number || t("general.labels.na")} @@ -40,14 +40,18 @@ export default function CsiResponseListPaginated({ }, { title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), - sortOrder: sortcolumn === "owner" && sortorder, + dataIndex: "owner_name", + key: "owner_name", + sorter: (a, b) => + alphaSort( + OwnerNameDisplayFunction(a.job), + OwnerNameDisplayFunction(b.job) + ), + sortOrder: + state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order, render: (text, record) => { - return record.job.owner ? ( - + return record.job.ownerid ? ( + ) : ( @@ -62,8 +66,9 @@ export default function CsiResponseListPaginated({ dataIndex: "completedon", key: "completedon", ellipsis: true, - sorter: (a, b) => a.completedon - b.completedon, - sortOrder: sortcolumn === "completedon" && sortorder, + sorter: (a, b) => dateSort(a.completedon, b.completedon), + sortOrder: + state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, render: (text, record) => { return record.completedon ? ( {record.completedon} @@ -73,25 +78,21 @@ export default function CsiResponseListPaginated({ ]; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); - search.page = pagination.current; - search.sortcolumn = sorter.columnKey; - search.sortorder = sorter.order; - history.push({ search: queryString.stringify(search) }); + setState({ + ...state, + filteredInfo: filters, + sortedInfo: sorter, + page: pagination.current, + }); }; const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - search.responseid = record.id; - history.push({ search: queryString.stringify(search) }); - } + if (record?.id) { + setresponseid(record.id); } else { - delete search.responseid; - history.push({ search: queryString.stringify(search) }); + setresponseid(""); } }; - return ( { handleOnRowClick(record); }, - selectedRowKeys: [responseid], type: "radio", }} onRow={(record, rowIndex) => { diff --git a/client/src/graphql/csi.queries.js b/client/src/graphql/csi.queries.js index f835f1d56..d2b62af43 100644 --- a/client/src/graphql/csi.queries.js +++ b/client/src/graphql/csi.queries.js @@ -57,19 +57,15 @@ export const INSERT_CSI = gql` `; export const QUERY_CSI_RESPONSE_PAGINATED = gql` - query QUERY_CSI_RESPONSE_PAGINATED( - $offset: Int - $limit: Int - $order: [csi_order_by!]! - ) { - csi(offset: $offset, limit: $limit, order_by: $order) { + query QUERY_CSI_RESPONSE_PAGINATED { + csi(order_by: { completedon: desc_nulls_last }) { id completedon job { ownr_fn ownr_ln + ownerid ro_number - id } } @@ -83,6 +79,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql` export const QUERY_CSI_RESPONSE_BY_PK = gql` query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) { csi_by_pk(id: $id) { + completedon relateddata valid validuntil diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index f0c295685..670f578ad 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -1,22 +1,20 @@ -import { Row, Col } from "antd"; import { useQuery } from "@apollo/client"; -import queryString from "query-string"; -import React, { useEffect } from "react"; +import { Col, Row } from "antd"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import AlertComponent from "../../components/alert/alert.component"; import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container"; import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component"; +import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries"; import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; -import {pageLimit} from "../../utils/config"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -32,29 +30,13 @@ export function ShopCsiContainer({ setSelectedHeader, }) { const { t } = useTranslation(); - - const searchParams = queryString.parse(useLocation().search); - const { page, sortcolumn, sortorder } = searchParams; + const [responseid, setresponseid] = useState(""); const { loading, error, data, refetch } = useQuery( QUERY_CSI_RESPONSE_PAGINATED, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", - variables: { - //search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, - order: [ - { - [sortcolumn || "completedon"]: sortorder - ? sortorder === "descend" - ? "desc_nulls_last" - : "asc" - : "desc_nulls_last", - }, - ], - }, } ); @@ -73,12 +55,7 @@ export function ShopCsiContainer({ if (error) return ; return ( - - // } - > +
- + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c73b84bef..51e45ef05 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -845,7 +845,8 @@ "fields": { "completedon": "Completed On", "created_at": "Created At", - "surveyid": "Survey ID {{surveyId}}" + "surveyid": "Survey ID {{surveyId}}", + "validuntil": "Valid Until" }, "labels": { "nologgedinuser": "Please log out of ImEX Online", From 205d50709712a02b938037f37e4a3da0760350ea Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 2 Feb 2024 22:50:40 -0800 Subject: [PATCH 074/131] IO-2626 Update Translations --- .../shop-csi-config/shop-csi-config.component.jsx | 2 +- client/src/translations/en_us/common.json | 6 +++--- client/src/translations/es/common.json | 13 ++++++++++--- client/src/translations/fr/common.json | 13 ++++++++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/client/src/components/shop-csi-config/shop-csi-config.component.jsx b/client/src/components/shop-csi-config/shop-csi-config.component.jsx index 2851d1f41..b6afe1ae5 100644 --- a/client/src/components/shop-csi-config/shop-csi-config.component.jsx +++ b/client/src/components/shop-csi-config/shop-csi-config.component.jsx @@ -41,7 +41,7 @@ export default function ShopCsiConfig() { )} /> - + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 51e45ef05..d074e3700 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -849,8 +849,8 @@ "validuntil": "Valid Until" }, "labels": { - "nologgedinuser": "Please log out of ImEX Online", - "nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.", + "nologgedinuser": "Please log out of $t(titles.app)", + "nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.", "noneselected": "No response selected.", "title": "Customer Satisfaction Survey", "greeting": "Hi {{name}}!", @@ -858,7 +858,7 @@ "copyright": "Copyright © $t(titles.app). All Rights Reserved." }, "successes": { - "created": "CSI created successfully. ", + "created": "CSI created successfully.", "submitted": "Your responses have been submitted successfully.", "submittedsub": "Your input is highly appreciated." } diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 0b7722676..11b80433b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -838,17 +838,24 @@ "creating": "", "notconfigured": "", "notfoundsubtitle": "", - "notfoundtitle": "" + "notfoundtitle": "", + "surveycompletetitle": "", + "surveycompletesubtitle": "" }, "fields": { "completedon": "", - "created_at": "" + "created_at": "", + "surveyid": "", + "validuntil": "" }, "labels": { "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "" + "title": "", + "greeting": "", + "intro": "", + "copyright": "" }, "successes": { "created": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 86ebd85ec..7dfd45642 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -838,17 +838,24 @@ "creating": "", "notconfigured": "", "notfoundsubtitle": "", - "notfoundtitle": "" + "notfoundtitle": "", + "surveycompletetitle": "", + "surveycompletesubtitle": "" }, "fields": { "completedon": "", - "created_at": "" + "created_at": "", + "surveyid": "", + "validuntil": "" }, "labels": { "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "" + "title": "", + "greeting": "", + "intro": "", + "copyright": "" }, "successes": { "created": "", From 9383b37a416b9fee85db7f15360d5f32cf7d7fec Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 5 Feb 2024 20:03:17 -0800 Subject: [PATCH 075/131] IO-2626 Modify seachparm and fix linking --- .../csi-response-form.container.jsx | 17 ++++------- .../csi-response-list-paginated.component.jsx | 30 ++++++++++--------- .../shop-csi/shop-csi.container.page.jsx | 6 ++-- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/client/src/components/csi-response-form/csi-response-form.container.jsx b/client/src/components/csi-response-form/csi-response-form.container.jsx index 38b06744d..d7a565608 100644 --- a/client/src/components/csi-response-form/csi-response-form.container.jsx +++ b/client/src/components/csi-response-form/csi-response-form.container.jsx @@ -1,19 +1,20 @@ import { useQuery } from "@apollo/client"; import { Card, Form, Result } from "antd"; -// import queryString from "query-string"; +import queryString from "query-string"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -// import { useLocation } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; import { DateFormatter } from "../../utils/DateFormatter"; import AlertComponent from "../alert/alert.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; -export default function CsiResponseFormContainer({ responseid }) { +export default function CsiResponseFormContainer() { const { t } = useTranslation(); const [form] = Form.useForm(); - + const searchParams = queryString.parse(useLocation().search); + const { responseid } = searchParams; const { loading, error, data } = useQuery(QUERY_CSI_RESPONSE_BY_PK, { variables: { id: responseid, @@ -44,13 +45,7 @@ export default function CsiResponseFormContainer({ responseid }) { readOnly componentList={data.csi_by_pk.csiquestion.config} /> - {data.csi_by_pk.completedon ? ( - <> - {t("csi.fields.completedon")} - {": "} - {data.csi_by_pk.completedon} - - ) : data.csi_by_pk.validuntil ? ( + {data.csi_by_pk.validuntil ? ( <> {t("csi.fields.validuntil")} {": "} diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index b41011f07..3d1a3b864 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -1,8 +1,9 @@ import { SyncOutlined } from "@ant-design/icons"; import { Button, Card, Table } from "antd"; +import queryString from "query-string"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; +import { Link, useHistory, useLocation } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; import { pageLimit } from "../../utils/config"; import { alphaSort, dateSort } from "../../utils/sorters"; @@ -15,21 +16,23 @@ export default function CsiResponseListPaginated({ loading, responses, total, - setresponseid, }) { + const search = queryString.parse(useLocation().search); + const { responseid } = search; + const history = useHistory(); + const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, filteredInfo: { text: "" }, page: "", }); - const { t } = useTranslation(); const columns = [ { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - sorter: (a, b) => alphaSort(a.job?.ro_number, b.job?.ro_number), + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( @@ -87,12 +90,17 @@ export default function CsiResponseListPaginated({ }; const handleOnRowClick = (record) => { - if (record?.id) { - setresponseid(record.id); + if (record) { + if (record.id) { + search.responseid = record.id; + history.push({ search: queryString.stringify(search) }); + } } else { - setresponseid(""); + delete search.responseid; + history.push({ search: queryString.stringify(search) }); } }; + return ( { handleOnRowClick(record); }, + selectedRowKeys: [responseid], type: "radio", }} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); - }, // click row - }; - }} /> ); diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index 670f578ad..e9d304b53 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -1,6 +1,6 @@ import { useQuery } from "@apollo/client"; import { Col, Row } from "antd"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -30,7 +30,6 @@ export function ShopCsiContainer({ setSelectedHeader, }) { const { t } = useTranslation(); - const [responseid, setresponseid] = useState(""); const { loading, error, data, refetch } = useQuery( QUERY_CSI_RESPONSE_PAGINATED, @@ -63,11 +62,10 @@ export function ShopCsiContainer({ loading={loading} responses={data ? data.csi : []} total={data ? data.csi_aggregate.aggregate.count : 0} - setresponseid={setresponseid} /> - + From 7c303a51548a531d2fd97c34bb84fd38c29bdc91 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 5 Feb 2024 20:06:28 -0800 Subject: [PATCH 076/131] IO-2626 Change Server Variable Name for response --- server/csi/lookup.js | 4 ++-- server/csi/submit.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/csi/lookup.js b/server/csi/lookup.js index 48154cdb8..81a44c2b8 100644 --- a/server/csi/lookup.js +++ b/server/csi/lookup.js @@ -13,10 +13,10 @@ const client = require("../graphql-client/graphql-client").client; exports.default = async (req, res) => { try { logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null); - const response = await client.request(queries.QUERY_SURVEY, { + const gql_response = await client.request(queries.QUERY_SURVEY, { surveyId: req.body.surveyId, }); - res.status(200).json(response); + res.status(200).json(gql_response); } catch (error) { logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error); res.status(400).json(error); diff --git a/server/csi/submit.js b/server/csi/submit.js index da5727531..a232425ae 100644 --- a/server/csi/submit.js +++ b/server/csi/submit.js @@ -13,7 +13,7 @@ const client = require("../graphql-client/graphql-client").client; exports.default = async (req, res) => { try { logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null); - const response = await client.request(queries.COMPLETE_SURVEY, { + const gql_response = await client.request(queries.COMPLETE_SURVEY, { surveyId: req.body.surveyId, survey: { response: req.body.values, @@ -21,7 +21,7 @@ exports.default = async (req, res) => { completedon: new Date(), }, }); - res.status(200).json(response); + res.status(200).json(gql_response); } catch (error) { logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error); res.status(400).json(error); From 3110be47034455d63171967d667b71990a2d9a08 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 6 Feb 2024 08:52:51 -0800 Subject: [PATCH 077/131] IO-2626 CICD Resource Size Change --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 437f570d9..61cfe17e3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: app-build: docker: - image: cimg/node:16.15.0 - + resource_class: large working_directory: ~/repo/client steps: @@ -159,4 +159,4 @@ workflows: #- admin-app-build: #filters: #branches: - #only: master \ No newline at end of file + #only: master From 616a4b04a09a96cd112708ca3159ff2267e851e2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 6 Feb 2024 09:10:32 -0800 Subject: [PATCH 078/131] IO-2626 Resource Class for Test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 61cfe17e3..5d6f8f506 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,7 +83,7 @@ jobs: test-app-build: docker: - image: cimg/node:16.15.0 - + resource_class: large working_directory: ~/repo/client steps: From dd5ca5d2339ab3edfca0b45bd1ad88a00833623f Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 6 Feb 2024 09:29:22 -0800 Subject: [PATCH 079/131] Add resource class to test build as well. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 61cfe17e3..5d6f8f506 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,7 +83,7 @@ jobs: test-app-build: docker: - image: cimg/node:16.15.0 - + resource_class: large working_directory: ~/repo/client steps: From 3c7ede0155f10ab89df5699a4fc0095ec65a1791 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 6 Feb 2024 10:04:59 -0800 Subject: [PATCH 080/131] IO-2626 Prevent crisp from loading on anon CSI page. --- client/src/pages/csi/csi.container.page.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index 96b9663a9..e3bdcae80 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -31,6 +31,11 @@ export function CsiContainerPage({ currentUser }) { const getAxiosData = useCallback(async () => { try { + try { + window.$crisp.push(["do", "chat:hide"]); + } catch { + console.log("Unable to attach to crisp instance. "); + } setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true })); const response = await axios.post("/csi/lookup", { surveyId }); setSubmitting((prevSubmitting) => ({ From 67008c35b8ac99f8865dc967175bc715ceb6e9f0 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 8 Feb 2024 09:57:39 -0800 Subject: [PATCH 081/131] IO-2626 Adjust Image Prop on customer page --- client/src/pages/csi/csi.container.page.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index e3bdcae80..74113ae20 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -143,7 +143,12 @@ export function CsiContainerPage({ currentUser }) { >
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( - Logo + {bodyshop.shopname.concat(" ) : null}
From 05a5df789bcf55851a4be3f76ba3e997110a3c7c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 8 Feb 2024 13:22:00 -0800 Subject: [PATCH 082/131] IO-2030 Change & Add Columns, Add Sorters and Filters --- client/src/graphql/jobs.queries.js | 5 +- .../parts-queue.page.component.jsx | 111 +++++++++++------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 57bb935f8..72ecd6897 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -112,7 +112,6 @@ export const QUERY_PARTS_QUEUE = gql` $statuses: [String!]! $offset: Int $limit: Int - $order: [jobs_order_by!] ) { jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) { aggregate { @@ -125,7 +124,7 @@ export const QUERY_PARTS_QUEUE = gql` } offset: $offset limit: $limit - order_by: $order + order_by: { ro_number: desc } ) { ownr_fn ownr_ln @@ -142,7 +141,9 @@ export const QUERY_PARTS_QUEUE = gql` v_color vehicleid scheduled_in + scheduled_completion id + ins_co_nm clm_no ro_number status diff --git a/client/src/pages/parts-queue/parts-queue.page.component.jsx b/client/src/pages/parts-queue/parts-queue.page.component.jsx index 62970d788..a9d66b363 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.component.jsx @@ -11,14 +11,17 @@ import { createStructuredSelector } from "reselect"; import AlertComponent from "../../components/alert/alert.component"; import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component"; import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component"; -import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../../components/owner-name-display/owner-name-display.component"; import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { pageLimit } from "../../utils/config"; import { alphaSort, dateSort } from "../../utils/sorters"; import useLocalStorage from "../../utils/useLocalStorage"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -26,12 +29,7 @@ const mapStateToProps = createStructuredSelector({ export function PartsQueuePageComponent({ bodyshop }) { const searchParams = queryString.parse(useLocation().search); - const { - //page, - sortcolumn, - sortorder, - statusFilters, - } = searchParams; + const { sortcolumn, sortorder, statusFilters } = searchParams; const history = useHistory(); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); @@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - // offset: page ? (page - 1) * 25 : 0, - // limit: 25, statuses: (statusFilters && JSON.parse(statusFilters)) || bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - order: [ - { - [sortcolumn || "ro_number"]: sortorder - ? sortorder === "descend" - ? "desc" - : "asc" - : "desc", - }, - ], }, }); @@ -125,7 +112,8 @@ export function PartsQueuePageComponent({ bodyshop }) { title: t("jobs.fields.owner"), dataIndex: "ownr_ln", key: "ownr_ln", - sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sortOrder: sortcolumn === "ownr_ln" && sortorder, render: (text, record) => { return record.ownerid ? ( @@ -139,6 +127,56 @@ export function PartsQueuePageComponent({ bodyshop }) { ); }, }, + { + 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: sortcolumn === "vehicle" && sortorder, + render: (text, record) => { + return record.vehicleid ? ( + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("jobs.fields.ins_co_nm_short"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: sortcolumn === "ins_co_nm" && sortorder, + filteredValue: filter?.ins_co_nm || null, + filters: + (jobs && + jobs + .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("jobs.fields.status"), dataIndex: "status", @@ -170,23 +208,16 @@ export function PartsQueuePageComponent({ bodyshop }) { ), }, { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: sortcolumn === "scheduled_completion" && sortorder, + render: (text, record) => ( + {record.scheduled_completion} + ), }, // { // title: t("vehicles.fields.plate_no"), @@ -198,14 +229,6 @@ export function PartsQueuePageComponent({ bodyshop }) { // return record.plate_no ? record.plate_no : ""; // }, // }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: sortcolumn === "clm_no" && sortorder, - }, // { // title: t("jobs.fields.clm_total"), // dataIndex: "clm_total", From f9b9f39418585d0dbe59948345588279480d0ce7 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 8 Feb 2024 17:18:14 -0800 Subject: [PATCH 083/131] IO-2630 Add in Drawer for Parts Queue --- .../parts-queue-card.component.jsx | 77 +++++++ .../parts-queue-job-lines.component.jsx | 208 ++++++++++++++++++ .../parts-queue.list.component.jsx} | 40 +++- client/src/graphql/jobs.queries.js | 169 +++++++++++++- .../parts-queue.page.container.jsx | 6 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 8 files changed, 486 insertions(+), 17 deletions(-) create mode 100644 client/src/components/parts-queue-card/parts-queue-card.component.jsx create mode 100644 client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx rename client/src/{pages/parts-queue/parts-queue.page.component.jsx => components/parts-queue-list/parts-queue.list.component.jsx} (90%) diff --git a/client/src/components/parts-queue-card/parts-queue-card.component.jsx b/client/src/components/parts-queue-card/parts-queue-card.component.jsx new file mode 100644 index 000000000..c703b7616 --- /dev/null +++ b/client/src/components/parts-queue-card/parts-queue-card.component.jsx @@ -0,0 +1,77 @@ +import { useQuery } from "@apollo/client"; +import { Card, Divider, Drawer, Grid } from "antd"; +import queryString from "query-string"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useHistory, useLocation } from "react-router-dom"; +import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries"; +import AlertComponent from "../alert/alert.component"; +import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component"; + +export default function PartsQueueDetailCard() { + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; + + const bpoints = { + xs: "100%", + sm: "100%", + md: "100%", + lg: "75%", + xl: "75%", + xxl: "60%", + }; + const drawerPercentage = selectedBreakpoint + ? bpoints[selectedBreakpoint[0]] + : "100%"; + + const searchParams = queryString.parse(useLocation().search); + const { selected } = searchParams; + const history = useHistory(); + const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, { + variables: { id: selected }, + skip: !selected, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + const { t } = useTranslation(); + const handleDrawerClose = () => { + delete searchParams.selected; + history.push({ + search: queryString.stringify({ + ...searchParams, + }), + }); + }; + + return ( + + {loading ? : null} + {error ? : null} + {data ? ( + + {data.jobs_by_pk.ro_number || t("general.labels.na")} + + } + > + + + + + ) : null} + + ); +} diff --git a/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx new file mode 100644 index 000000000..d680c1bef --- /dev/null +++ b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx @@ -0,0 +1,208 @@ +import { Card, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { alphaSort } from "../../utils/sorters"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: {}, + }); + const { t } = useTranslation(); + + const columns = [ + { + title: "#", + dataIndex: "line_no", + key: "line_no", + sorter: (a, b) => a.line_no - b.line_no, + sortOrder: + state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order, + }, + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + onCell: (record) => ({ + className: record.manual_line && "job-line-manual", + style: { + ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}), + }, + }), + sortOrder: + state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, + ellipsis: true, + }, + { + title: t("joblines.fields.oem_partno"), + dataIndex: "oem_partno", + key: "oem_partno", + sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno), + sortOrder: + state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order, + ellipsis: true, + render: (text, record) => + `${record.oem_partno || ""} ${ + record.alt_partno ? `(${record.alt_partno})` : "" + }`.trim(), + }, + { + title: t("joblines.fields.part_type"), + dataIndex: "part_type", + key: "part_type", + filteredValue: state.filteredInfo.part_type || null, + sorter: (a, b) => alphaSort(a.part_type, b.part_type), + sortOrder: + state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order, + filters: [ + { + text: t("jobs.labels.partsfilter"), + value: [ + "PAN", + "PAC", + "PAR", + "PAL", + "PAA", + "PAM", + "PAP", + "PAS", + "PASL", + "PAG", + ], + }, + { + text: t("joblines.fields.part_types.PAN"), + value: ["PAN"], + }, + { + text: t("joblines.fields.part_types.PAP"), + value: ["PAP"], + }, + { + text: t("joblines.fields.part_types.PAL"), + value: ["PAL"], + }, + { + text: t("joblines.fields.part_types.PAA"), + value: ["PAA"], + }, + { + text: t("joblines.fields.part_types.PAG"), + value: ["PAG"], + }, + { + text: t("joblines.fields.part_types.PAS"), + value: ["PAS"], + }, + { + text: t("joblines.fields.part_types.PASL"), + value: ["PASL"], + }, + { + text: t("joblines.fields.part_types.PAC"), + value: ["PAC"], + }, + { + text: t("joblines.fields.part_types.PAR"), + value: ["PAR"], + }, + { + text: t("joblines.fields.part_types.PAM"), + value: ["PAM"], + }, + ], + onFilter: (value, record) => value.includes(record.part_type), + render: (text, record) => + record.part_type + ? t(`joblines.fields.part_types.${record.part_type}`) + : null, + }, + { + title: t("joblines.fields.part_qty"), + dataIndex: "part_qty", + key: "part_qty", + }, + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + sorter: (a, b) => a.act_price - b.act_price, + sortOrder: + state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, + ellipsis: true, + render: (text, record) => ( + + {record.db_ref === "900510" || record.db_ref === "900511" + ? record.prt_dsmk_m + : record.act_price} + + ), + }, + { + title: t("joblines.fields.location"), + dataIndex: "location", + key: "location", + }, + { + title: t("joblines.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: state.filteredInfo.status || null, + filters: + (jobLines && + jobLines + .map((l) => l.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + })) || + [], + onFilter: (value, record) => value.includes(record.status), + }, + ]; + + const handleTableChange = (pagination, filters, sorter) => { + setState((state) => ({ + ...state, + filteredInfo: filters, + sortedInfo: sorter, + })); + }; + + return ( + +
+ + ); +} +export default connect( + mapStateToProps, + mapDispatchToProps +)(PartsQueueJobLinesComponent); diff --git a/client/src/pages/parts-queue/parts-queue.page.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx similarity index 90% rename from client/src/pages/parts-queue/parts-queue.page.component.jsx rename to client/src/components/parts-queue-list/parts-queue.list.component.jsx index a9d66b363..44d5368ce 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx @@ -8,13 +8,6 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import AlertComponent from "../../components/alert/alert.component"; -import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component"; -import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component"; -import OwnerNameDisplay, { - OwnerNameDisplayFunction, -} from "../../components/owner-name-display/owner-name-display.component"; -import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; @@ -22,14 +15,21 @@ import { onlyUnique } from "../../utils/arrayHelper"; import { pageLimit } from "../../utils/config"; import { alphaSort, dateSort } from "../../utils/sorters"; import useLocalStorage from "../../utils/useLocalStorage"; +import AlertComponent from "../alert/alert.component"; +import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; +import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; +import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); -export function PartsQueuePageComponent({ bodyshop }) { +export function PartsQueueListComponent({ bodyshop }) { const searchParams = queryString.parse(useLocation().search); - const { sortcolumn, sortorder, statusFilters } = searchParams; + const { selected, sortcolumn, sortorder, statusFilters } = searchParams; const history = useHistory(); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); @@ -94,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) { history.push({ search: queryString.stringify(searchParams) }); }; + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + history.push({ + search: queryString.stringify({ + ...searchParams, + selected: record.id, + }), + }); + } + } + }; + const columns = [ { title: t("jobs.fields.ro_number"), @@ -330,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) { style={{ height: "100%" }} scroll={{ x: true }} onChange={handleTableChange} + rowSelection={{ + onSelect: (record) => { + handleOnRowClick(record); + }, + selectedRowKeys: [selected], + type: "radio", + }} /> ); } -export default connect(mapStateToProps, null)(PartsQueuePageComponent); +export default connect(mapStateToProps, null)(PartsQueueListComponent); diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 72ecd6897..ee4c49dcc 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -108,11 +108,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql` `; export const QUERY_PARTS_QUEUE = gql` - query QUERY_PARTS_QUEUE( - $statuses: [String!]! - $offset: Int - $limit: Int - ) { + query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) { jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) { aggregate { count(distinct: true) @@ -2339,3 +2335,166 @@ export const MARK_JOB_AS_UNINVOICED = gql` } } `; + +export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql` + query QUERY_JOB_CARD_DETAILS($id: uuid!) { + jobs_by_pk(id: $id) { + actual_completion + actual_delivery + actual_in + alt_transport + available_jobs { + id + } + area_of_damage + ca_gst_registrant + cccontracts { + agreementnumber + courtesycar { + id + make + model + year + plate + fleetnumber + } + id + scheduledreturn + start + status + } + clm_no + clm_total + comment + date_estimated + date_exported + date_invoiced + date_last_contacted + date_next_contact + date_open + date_repairstarted + date_scheduled + ded_amt + employee_body + employee_body_rel { + id + first_name + last_name + } + employee_csr + employee_csr_rel { + id + first_name + last_name + } + employee_prep + employee_prep_rel { + id + first_name + last_name + } + employee_refinish + employee_refinish_rel { + id + first_name + last_name + } + est_co_nm + est_ct_fn + est_ct_ln + est_ea + est_ph1 + id + ins_co_nm + ins_ct_fn + ins_ct_ln + ins_ea + ins_ph1 + inproduction + job_totals + joblines( + order_by: { line_no: asc } + where: { + act_price: { _neq: 0 } + part_type: { + _in: [ + "PAN" + "PAC" + "PAR" + "PAL" + "PAA" + "PAM" + "PAP" + "PAS" + "PASL" + "PAG" + ] + } + removed: { _eq: false } + } + ) { + act_price + alt_partno + db_ref + id + line_desc + line_no + location + mod_lbr_ty + mod_lb_hrs + oem_partno + part_qty + part_type + prt_dsmk_m + status + } + lbr_adjustments + ownr_co_nm + ownr_ea + ownr_fn + ownr_ln + ownr_ph1 + ownr_ph2 + owner { + id + allow_text_message + preferred_contact + tax_number + } + owner_owing + plate_no + plate_st + po_number + production_vars + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + special_coverage_policy + status + suspended + updated_at + vehicle { + id + jobs { + id + clm_no + ro_number + } + notes + plate_no + v_color + v_make_desc + v_model_desc + v_model_yr + } + vehicleid + v_color + v_make_desc + v_model_desc + v_model_yr + v_vin + voided + } + } +`; diff --git a/client/src/pages/parts-queue/parts-queue.page.container.jsx b/client/src/pages/parts-queue/parts-queue.page.container.jsx index 5f5d8d617..3f78e9b92 100644 --- a/client/src/pages/parts-queue/parts-queue.page.container.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.container.jsx @@ -1,12 +1,13 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component"; +import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; -import PartsQueuePage from "./parts-queue.page.component"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) { return ( - + + ); } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 7679ffeed..f1e514b90 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1822,6 +1822,7 @@ "override_header": "Override estimate header on import?", "ownerassociation": "Owner Association", "parts": "Parts", + "parts_lines": "Parts Lines", "parts_received": "Parts Rec.", "parts_tax_rates": "Parts Tax rates", "partsfilter": "Parts Only", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 0b7722676..eac477911 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1822,6 +1822,7 @@ "override_header": "¿Anular encabezado estimado al importar?", "ownerassociation": "", "parts": "Partes", + "parts_lines": "", "parts_received": "", "parts_tax_rates": "", "partsfilter": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 86ebd85ec..a70920b18 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1822,6 +1822,7 @@ "override_header": "Remplacer l'en-tête d'estimation à l'importation?", "ownerassociation": "", "parts": "les pièces", + "parts_lines": "", "parts_received": "", "parts_tax_rates": "", "partsfilter": "", From 30cf46a15826e4eac37cde892ef6b054256b055e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 9 Feb 2024 09:54:22 -0800 Subject: [PATCH 084/131] IO-2630 Adjust Query to match Tags --- client/src/graphql/jobs.queries.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index ee4c49dcc..650abc302 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2415,7 +2415,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql` joblines( order_by: { line_no: asc } where: { - act_price: { _neq: 0 } part_type: { _in: [ "PAN" @@ -2425,8 +2424,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql` "PAA" "PAM" "PAP" - "PAS" - "PASL" "PAG" ] } From 2584f7129c23e77a124a9540a6faa1a24d57f6dd Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 14 Feb 2024 14:31:35 -0500 Subject: [PATCH 085/131] - Report Center Filters Version 1 retargeted to Master Signed-off-by: Dave Richer --- _reference/reportFiltersAndSorters.md | 120 +++++ ...center-modal-filters-sorters-component.jsx | 261 +++++++++ .../report-center-modal.component.jsx | 510 +++++++++--------- client/src/translations/en_us/common.json | 5 + client/src/translations/es/common.json | 5 + client/src/translations/fr/common.json | 5 + client/src/utils/RenderTemplate.js | 336 ++++++++---- client/src/utils/graphQLmodifier.js | 309 +++++++++++ 8 files changed, 1174 insertions(+), 377 deletions(-) create mode 100644 _reference/reportFiltersAndSorters.md create mode 100644 client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx create mode 100644 client/src/utils/graphQLmodifier.js diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md new file mode 100644 index 000000000..bcaa08ade --- /dev/null +++ b/_reference/reportFiltersAndSorters.md @@ -0,0 +1,120 @@ +# Filters and Sorters + +This documentation details the schema required for `.filters` files on the report server. It is used to dynamically +modify the graphQL query and provide the user more power over their reports. + +## High level Schema Overview + +```javascript +const schema = { + "filters": [ + { + "name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query + "translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI + "label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation + "type": "number" // Type of field, can be number or string currently + }, + // ... more filters + ], + "sorters": [ + { + "name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query + "translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI + "label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation + "type": "number" // Type of field, can be number or string currently + }, + // ... more sorters + ], + "dates": { + // This is not yet implemented and will be added in a future release + } +} +``` + +## Filters + +Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server. +A note on special notation used in the `name` field. + +### Path without brackets, multi level + +`"name": "jobs.joblines.mod_lb_hrs",` +This will produce a where clause at the `joblines` level of the graphQL query, + +```graphql +query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) { + jobs( + where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}} + ) { + joblines( + order_by: {line_no: asc} + where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}} + ) { + line_no + mod_lbr_ty + mod_lb_hrs + convertedtolbr + convertedtolbr_data + } + ownr_co_nm + ownr_fn + ownr_ln + plate_no + ro_number + status + v_make_desc + v_model_desc + v_model_yr + v_vin + v_color + } +} +``` + + +### Path with brackets,top level +`"name": "[jobs].joblines.mod_lb_hrs",` +This will produce a where clause at the `jobs` level of the graphQL query. + +```graphql +query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) { + jobs( + where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}} + ) { + joblines( + order_by: {line_no: asc} + where: {removed: {_eq: false}} + ) { + line_no + mod_lbr_ty + mod_lb_hrs + convertedtolbr + convertedtolbr_data + } + ownr_co_nm + ownr_fn + ownr_ln + plate_no + ro_number + status + v_make_desc + v_model_desc + v_model_yr + v_vin + v_color + } +} +``` + +## Known Caveats +- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. +- The `dates` object is not yet implemented and will be added in a future release. +- The type object must be 'string' or 'number' and is case-sensitive. +- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. +- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues. +- Do not add the ability to filter on things like FK constraints, must like the above example. + + +## Sorters +- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. +- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx new file mode 100644 index 000000000..98293a607 --- /dev/null +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -0,0 +1,261 @@ +import {Button, Card, Col, Form, Input, InputNumber, Row, Select} from "antd"; +import React, {useEffect, useState} from "react"; +import {fetchFilterData} from "../../utils/RenderTemplate"; +import {DeleteFilled} from "@ant-design/icons"; +import {useTranslation} from "react-i18next"; +import {getOperatorsByType} from "../../utils/graphQLmodifier"; + +export default function ReportCenterModalFiltersSortersComponent({form}) { + return ( + + {() => { + const key = form.getFieldValue("key"); + return ; + }} + + ); +} + +function RenderFilters({templateId, form}) { + const [state, setState] = useState(null); + const [visible, setVisible] = useState(false); + const {t} = useTranslation(); + + useEffect(() => { + const fetch = async () => { + const data = await fetchFilterData({name: templateId}); + if (data?.success) { + setState(data.data); + } else { + setState(null); + } + }; + + if (templateId) { + fetch(); + } + }, [templateId]); + + + if (!templateId || !state) return null; + return ( + + + + {visible && ( +
+ {state.filters && state.filters.length > 0 && ( + + + {(fields, {add, remove, move}) => { + return ( +
+ {fields.map((field, index) => ( + + +
+ + + + } + } + + + + + + { + () => { + const name = form.getFieldValue(['filters', field.name, "field"]); + const type = state.filters.find(f => f.name === name)?.type; + + return + {type === 'number' ? + { + form.setFieldsValue({[field.name]: {value: parseInt(value)}}); + }} + /> + : + { + form.setFieldsValue({[field.name]: {value: value.toString()}}); + }} + /> + } + + } + } + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + + )} + {state.sorters && state.sorters.length > 0 && ( + + + {(fields, {add, remove, move}) => { + return ( +
+ Sorters + {fields.map((field, index) => ( + + +
+ + + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + )} + + )} + + ); +} \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index c4bef11c3..af4ce7ef4 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -1,30 +1,22 @@ -import { useLazyQuery } from "@apollo/client"; -import { - Button, - Card, - Col, - DatePicker, - Form, - Input, - Radio, - Row, - Typography, -} from "antd"; +import {useLazyQuery} from "@apollo/client"; +import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd"; import _ from "lodash"; import moment from "moment"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries"; -import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; -import { selectReportCenter } from "../../redux/modals/modals.selectors"; -import DatePIckerRanges from "../../utils/DatePickerRanges"; -import { GenerateDocument } from "../../utils/RenderTemplate"; -import { TemplateList } from "../../utils/TemplateConstants"; +import React, {useState} from "react"; +import {useTranslation} from "react-i18next"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries"; +import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries"; +import {selectReportCenter} from "../../redux/modals/modals.selectors"; +import DatePickerRanges from "../../utils/DatePickerRanges"; +import {GenerateDocument} from "../../utils/RenderTemplate"; +import {TemplateList} from "../../utils/TemplateConstants"; import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import "./report-center-modal.styles.scss"; +import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; + const mapStateToProps = createStructuredSelector({ reportCenterModal: selectReportCenter, }); @@ -32,39 +24,39 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps, + mapDispatchToProps )(ReportCenterModalComponent); -export function ReportCenterModalComponent({ reportCenterModal }) { +export function ReportCenterModalComponent({reportCenterModal}) { const [form] = Form.useForm(); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); - const { t } = useTranslation(); + const {t} = useTranslation(); const Templates = TemplateList("report_center"); const ReportsList = Object.keys(Templates).map((key) => { return Templates[key]; }); - const { visible } = reportCenterModal; + const {open} = reportCenterModal; - const [callVendorQuery, { data: vendorData, called: vendorCalled }] = - useLazyQuery(QUERY_ALL_VENDORS, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callVendorQuery, {data: vendorData, called: vendorCalled}] = + useLazyQuery(QUERY_ALL_VENDORS, { + skip: !( + open && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); - const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] = - useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] = + useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { + skip: !( + open && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); const handleFinish = async (values) => { setLoading(true); @@ -73,243 +65,245 @@ export function ReportCenterModalComponent({ reportCenterModal }) { const { id } = values; await GenerateDocument( - { - name: values.key, - variables: { - ...(start - ? { start: moment(start).startOf("day").format("YYYY-MM-DD") } - : {}), - ...(end - ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } - : {}), - ...(start ? { starttz: moment(start).startOf("day") } : {}), - ...(end ? { endtz: moment(end).endOf("day") } : {}), + { + name: values.key, + variables: { + ...(start + ? { start: moment(start).startOf("day").format("YYYY-MM-DD") } + : {}), + ...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}), + ...(start ? { starttz: moment(start).startOf("day") } : {}), + ...(end ? { endtz: moment(end).endOf("day") } : {}), - ...(id ? { id: id } : {}), + ...(id ? { id: id } : {}), + }, + filters: values.filters, + sorters: values.sorters, }, - }, - { - to: values.to, - subject: Templates[values.key]?.subject, - }, - values.sendbyexcel === "excel" - ? "x" - : values.sendby === "email" - ? "e" - : "p", - id + { + to: values.to, + subject: Templates[values.key]?.subject, + }, + values.sendbyexcel === "excel" + ? "x" + : values.sendby === "email" + ? "e" + : "p", + id ); setLoading(false); }; const FilteredReportsList = - search !== "" - ? ReportsList.filter((r) => - r.title.toLowerCase().includes(search.toLowerCase()) - ) - : ReportsList; + search !== "" + ? ReportsList.filter((r) => + r.title.toLowerCase().includes(search.toLowerCase()) + ) + : ReportsList; //Group it, create cards, and then filter out. const grouped = _.groupBy(FilteredReportsList, "group"); return ( -
-
- setSearch(e.target.value)} - value={search} - /> - + - - {/* {Object.keys(Templates).map((key) => ( + setSearch(e.target.value)} + value={search} + /> + + + {/* {Object.keys(Templates).map((key) => ( {Templates[key].title} ))} */} - - {Object.keys(grouped).map((key) => ( -
- - - {t(`reportcenter.labels.groups.${key}`)} - -
    - {grouped[key].map((item) => ( -
  • - - {item.title} - -
  • - ))} -
-
- - ))} - - - - - {() => { - const key = form.getFieldValue("key"); - if (!key) return null; - //Kind of Id - const rangeFilter = Templates[key] && Templates[key].rangeFilter; - if (!rangeFilter) return null; - return ( -
- {t("reportcenter.labels.filterson", { - object: rangeFilter.object, - field: rangeFilter.field, - })} -
- ); - }} -
- - {() => { - const key = form.getFieldValue("key"); - const currentId = form.getFieldValue("id"); - if (!key) return null; - //Kind of Id - const idtype = Templates[key] && Templates[key].idtype; - if (!idtype && currentId) { - form.setFieldsValue({ id: null }); - return null; - } - if (!vendorCalled && idtype === "vendor") callVendorQuery(); - if (!employeeCalled && idtype === "employee") callEmployeeQuery(); - if (idtype === "vendor") + + {Object.keys(grouped).map((key) => ( + + + + {t(`reportcenter.labels.groups.${key}`)} + +
    + {grouped[key].map((item) => ( +
  • + + {item.title} + +
  • + ))} +
+
+ + ))} + + + + + {() => { + const key = form.getFieldValue("key"); + if (!key) return null; + //Kind of Id + const rangeFilter = Templates[key] && Templates[key].rangeFilter; + if (!rangeFilter) return null; return ( - - - +
+ {t("reportcenter.labels.filterson", { + object: rangeFilter.object, + field: rangeFilter.field, + })} +
); - if (idtype === "employee") - return ( - - - - ); - else return null; - }} -
- - {() => { - const key = form.getFieldValue("key"); - const datedisable = Templates[key] && Templates[key].datedisable; - if (datedisable !== true) { - return ( - - - - ); - } else return null; - }} - - - {() => { - const key = form.getFieldValue("key"); - //Kind of Id - const reporttype = Templates[key] && Templates[key].reporttype; + }} + + + + {() => { + const key = form.getFieldValue("key"); + const currentId = form.getFieldValue("id"); + if (!key) return null; + //Kind of Id + const idtype = Templates[key] && Templates[key].idtype; + if (!idtype && currentId) { + form.setFieldsValue({id: null}); + return null; + } + if (!vendorCalled && idtype === "vendor") callVendorQuery(); + if (!employeeCalled && idtype === "employee") callEmployeeQuery(); + if (idtype === "vendor") + return ( + + + + ); + if (idtype === "employee") + return ( + + + + ); + else return null; + }} + + + {() => { + const key = form.getFieldValue("key"); + const datedisable = Templates[key] && Templates[key].datedisable; + if (datedisable !== true) { + return ( + + + + ); + } else return null; + }} + + + {() => { + const key = form.getFieldValue("key"); + //Kind of Id + const reporttype = Templates[key] && Templates[key].reporttype; - if (reporttype === "excel") - return ( - - - {t("general.labels.excel")} - - - ); - if (reporttype !== "excel") - return ( - - - {t("general.labels.email")} - {t("general.labels.print")} - - - ); - }} - + if (reporttype === "excel") + return ( + + + {t("general.labels.excel")} + + + ); + if (reporttype !== "excel") + return ( + + + {t("general.labels.email")} + {t("general.labels.print")} + + + ); + }} + -
- -
- - +
+ +
+ + ); } + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index af575757c..aeeb7f89c 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2572,6 +2572,11 @@ "generate": "Generate" }, "labels": { + "advanced_filters": "Advanced Filters and Sorters", + "advanced_filters_show": "Show", + "advanced_filters_hide": "Hide", + "advanced_filters_filters": "Filters", + "advanced_filters_sorters": "Sorters", "dates": "Dates", "employee": "Employee", "filterson": "Filters on {{object}}: {{field}}", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 5e57e0911..4310e822a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2572,6 +2572,11 @@ "generate": "" }, "labels": { + "advanced_filters": "", + "advanced_filters_show": "", + "advanced_filters_hide": "", + "advanced_filters_filters": "", + "advanced_filters_sorters": "", "dates": "", "employee": "", "filterson": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 92616ae1c..b898ac1f4 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2572,6 +2572,11 @@ "generate": "" }, "labels": { + "advanced_filters": "", + "advanced_filters_show": "", + "advanced_filters_hide": "", + "advanced_filters_filters": "", + "advanced_filters_sorters": "", "dates": "", "employee": "", "filterson": "", diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 8cb4691fe..9c14eb31c 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -1,14 +1,16 @@ -import { gql } from "@apollo/client"; +import {gql} from "@apollo/client"; import jsreport from "@jsreport/browser-client"; -import { notification } from "antd"; +import {notification} from "antd"; import axios from "axios"; import _ from "lodash"; -import { auth } from "../firebase/firebase.utils"; -import { setEmailOptions } from "../redux/email/email.actions"; -import { store } from "../redux/store"; +import {auth} from "../firebase/firebase.utils"; +import {setEmailOptions} from "../redux/email/email.actions"; +import {store} from "../redux/store"; import client from "../utils/GraphQLClient"; import cleanAxios from "./CleanAxios"; -import { TemplateList } from "./TemplateConstants"; +import {TemplateList} from "./TemplateConstants"; +import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier"; + const server = process.env.REACT_APP_REPORTS_SERVER_URL; jsreport.serverUrl = server; @@ -16,11 +18,11 @@ jsreport.serverUrl = server; const Templates = TemplateList(); export default async function RenderTemplate( - templateObject, - bodyshop, - renderAsHtml = false, - renderAsExcel = false, - renderAsText = false + templateObject, + bodyshop, + renderAsHtml = false, + renderAsExcel = false, + renderAsText = false ) { if (window.jsr3) { jsreport.serverUrl = "https://reports3.test.imex.online/"; @@ -30,41 +32,41 @@ export default async function RenderTemplate( jsreport.headers["Authorization"] = jsrAuth; //Query assets that match the template name. Must be in format <>.query - let { contextData, useShopSpecificTemplate } = await fetchContextData( - templateObject, - jsrAuth + let {contextData, useShopSpecificTemplate} = await fetchContextData( + templateObject, + jsrAuth ); - const { ignoreCustomMargins } = Templates[templateObject.name]; + const {ignoreCustomMargins} = Templates[templateObject.name]; let reportRequest = { template: { name: useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${templateObject.name}` - : `/${templateObject.name}`, + ? `/${bodyshop.imexshopid}/${templateObject.name}` + : `/${templateObject.name}`, ...(renderAsHtml - ? {} - : { + ? {} + : { recipe: "chrome-pdf", ...(!ignoreCustomMargins && { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), }), - ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), - ...(renderAsText ? { recipe: "text" } : {}), + ...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}), + ...(renderAsText ? {recipe: "text"} : {}), }, data: { ...contextData, @@ -73,7 +75,7 @@ export default async function RenderTemplate( headerpath: `/${bodyshop.imexshopid}/header.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`, bodyshop: bodyshop, - offset: bodyshop.timezone, //moment().utcOffset(), + offset: bodyshop.timezone, //dayjs().utcOffset(), }, }; @@ -82,8 +84,8 @@ export default async function RenderTemplate( if (!renderAsHtml) { render.download( - (Templates[templateObject.name] && - Templates[templateObject.name].title) || + (Templates[templateObject.name] && + Templates[templateObject.name].title) || "" ); } else { @@ -97,17 +99,17 @@ export default async function RenderTemplate( ...(!ignoreCustomMargins && { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), }, @@ -121,21 +123,21 @@ export default async function RenderTemplate( resolve({ pdf, filename: - Templates[templateObject.name] && - Templates[templateObject.name].title, + Templates[templateObject.name] && + Templates[templateObject.name].title, html, }); }); } } catch (error) { - notification["error"]({ message: JSON.stringify(error) }); + notification["error"]({message: JSON.stringify(error)}); } } export async function RenderTemplates( - templateObjects, - bodyshop, - renderAsHtml = false + templateObjects, + bodyshop, + renderAsHtml = false ) { //Query assets that match the template name. Must be in format <>.query let unsortedTemplatesAndData = []; @@ -145,17 +147,18 @@ export async function RenderTemplates( templateObjects.forEach((template) => { proms.push( - (async () => { - let { contextData, useShopSpecificTemplate } = await fetchContextData( - template, - jsrAuth - ); - unsortedTemplatesAndData.push({ - templateObject: template, - contextData, - useShopSpecificTemplate, - }); - })() + (async () => { + console.log(' RENDER TEMPLATE 2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + let {contextData, useShopSpecificTemplate} = await fetchContextData( + template, + jsrAuth + ); + unsortedTemplatesAndData.push({ + templateObject: template, + contextData, + useShopSpecificTemplate, + }); + })() ); }); await Promise.all(proms); @@ -172,8 +175,8 @@ export async function RenderTemplates( unsortedTemplatesAndData.sort(function (a, b) { return ( - templateObjects.findIndex((x) => x.name === a.templateObject.name) - - templateObjects.findIndex((x) => x.name === b.templateObject.name) + templateObjects.findIndex((x) => x.name === a.templateObject.name) - + templateObjects.findIndex((x) => x.name === b.templateObject.name) ); }); const templateAndData = unsortedTemplatesAndData; @@ -183,25 +186,25 @@ export async function RenderTemplates( let reportRequest = { template: { name: rootTemplate.useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` - : `/${rootTemplate.templateObject.name}`, + ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` + : `/${rootTemplate.templateObject.name}`, ...(renderAsHtml - ? {} - : { + ? {} + : { recipe: "chrome-pdf", chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), pdfOperations: [ @@ -218,22 +221,22 @@ export async function RenderTemplates( template: { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, name: template.useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${template.templateObject.name}` - : `/${template.templateObject.name}`, - ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }), + ? `/${bodyshop.imexshopid}/${template.templateObject.name}` + : `/${template.templateObject.name}`, + ...(renderAsHtml ? {} : {recipe: "chrome-pdf"}), }, type: "append", @@ -245,8 +248,8 @@ export async function RenderTemplates( }, data: { ...extend( - rootTemplate.contextData, - ...templateAndData.map((temp) => temp.contextData) + rootTemplate.contextData, + ...templateAndData.map((temp) => temp.contextData) ), // ...rootTemplate.templateObject.variables, @@ -266,29 +269,29 @@ export async function RenderTemplates( return render.toString(); } } catch (error) { - notification["error"]({ message: JSON.stringify(error) }); + notification["error"]({message: JSON.stringify(error)}); } } export const GenerateDocument = async ( - template, - messageOptions, - sendType, - jobid + template, + messageOptions, + sendType, + jobid ) => { const bodyshop = store.getState().user.bodyshop; if (sendType === "e") { store.dispatch( - setEmailOptions({ - jobid, - messageOptions: { - ...messageOptions, - to: Array.isArray(messageOptions.to) - ? messageOptions.to - : [messageOptions.to], - }, - template, - }) + setEmailOptions({ + jobid, + messageOptions: { + ...messageOptions, + to: Array.isArray(messageOptions.to) + ? messageOptions.to + : [messageOptions.to], + }, + template, + }) ); } else if (sendType === "x") { console.log("excel"); @@ -305,22 +308,75 @@ export const GenerateDocuments = async (templates) => { await RenderTemplates(templates, bodyshop); }; +export const fetchFilterData = async ({name}) => { + try { + const bodyshop = store.getState().user.bodyshop; + const jsrAuth = (await axios.post("/utils/jsr")).data; + jsreport.headers["FirebaseAuthorization"] = + "Bearer " + (await auth.currentUser.getIdToken()); + + const folders = await cleanAxios.get(`${server}/odata/folders`, { + headers: {Authorization: jsrAuth}, + }); + const shopSpecificFolder = folders.data.value.find( + (f) => f.name === bodyshop.imexshopid + ); + + const jsReportFilters = await cleanAxios.get( + `${server}/odata/assets?$filter=name eq '${name}.filters'`, + {headers: {Authorization: jsrAuth}} + ); + console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters); + + let parsedFilterData; + let useShopSpecificTemplate = false; + // let shopSpecificTemplate; + + if (shopSpecificFolder) { + let shopSpecificTemplate = jsReportFilters.data.value.find( + (f) => f?.folder?.shortid === shopSpecificFolder.shortid + ); + if (shopSpecificTemplate) { + useShopSpecificTemplate = true; + parsedFilterData = atob(shopSpecificTemplate.content); + } + } + + if (!parsedFilterData) { + const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder); + useShopSpecificTemplate = false; + if (generalTemplate) parsedFilterData = atob(generalTemplate.content); + } + const data = JSON.parse(parsedFilterData); + return { + data, + useShopSpecificTemplate, + success: true, + } + } catch { + return { + success: false, + } + } +}; + const fetchContextData = async (templateObject, jsrAuth) => { + console.log(' FETCH CONTEXT DATA !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') const bodyshop = store.getState().user.bodyshop; jsreport.headers["FirebaseAuthorization"] = - "Bearer " + (await auth.currentUser.getIdToken()); + "Bearer " + (await auth.currentUser.getIdToken()); const folders = await cleanAxios.get(`${server}/odata/folders`, { - headers: { Authorization: jsrAuth }, + headers: {Authorization: jsrAuth}, }); const shopSpecificFolder = folders.data.value.find( - (f) => f.name === bodyshop.imexshopid + (f) => f.name === bodyshop.imexshopid ); const jsReportQueries = await cleanAxios.get( - `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, - { headers: { Authorization: jsrAuth } } + `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, + {headers: {Authorization: jsrAuth}} ); let templateQueryToExecute; @@ -329,7 +385,7 @@ const fetchContextData = async (templateObject, jsrAuth) => { if (shopSpecificFolder) { let shopSpecificTemplate = jsReportQueries.data.value.find( - (f) => f?.folder?.shortid === shopSpecificFolder.shortid + (f) => f?.folder?.shortid === shopSpecificFolder.shortid ); if (shopSpecificTemplate) { useShopSpecificTemplate = true; @@ -343,16 +399,58 @@ const fetchContextData = async (templateObject, jsrAuth) => { templateQueryToExecute = atob(generalTemplate.content); } + console.log('Template Object'); + console.dir(templateObject); + console.log('Unmodified Query'); + console.dir(templateQueryToExecute); + + + // We have no template filters or sorters, so we can just execute the query and return the data + if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) { + console.log('No filters or sorters'); + let contextData = {}; + if (templateQueryToExecute) { + const {data} = await client.query({ + query: gql(templateQueryToExecute), + variables: {...templateObject.variables}, + }); + contextData = data; + } + + return {contextData, useShopSpecificTemplate}; + } + + // Parse the query and apply the filters and sorters + const ast = parseQuery(templateQueryToExecute); + + let filterFields = []; + + if (templateObject?.filters && templateObject?.filters?.length) { + console.log('Applying filters') + applyFilters(ast, templateObject.filters, filterFields); + wrapFiltersInAnd(ast, filterFields); + } + + if (templateObject?.sorters && templateObject?.sorters?.length) { + console.log('Applying sorters') + applySorters(ast, templateObject.sorters); + } + + const finalQuery = printQuery(ast); + + console.log('Modified Query'); + console.log(finalQuery); + let contextData = {}; if (templateQueryToExecute) { - const { data } = await client.query({ - query: gql(templateQueryToExecute), - variables: { ...templateObject.variables }, + const {data} = await client.query({ + query: gql(finalQuery), + variables: {...templateObject.variables}, }); contextData = data; } - return { contextData, useShopSpecificTemplate }; + return {contextData, useShopSpecificTemplate}; }; //export const displayTemplateInWindow = (html) => { @@ -389,7 +487,7 @@ const fetchContextData = async (templateObject, jsrAuth) => { function extend(o1, o2, o3) { var result = {}, - obj; + obj; for (var i = 0; i < arguments.length; i++) { obj = arguments[i]; @@ -405,4 +503,4 @@ function extend(o1, o2, o3) { } } return result; -} +} \ No newline at end of file diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js new file mode 100644 index 000000000..5716753ad --- /dev/null +++ b/client/src/utils/graphQLmodifier.js @@ -0,0 +1,309 @@ +import {Kind, parse, print, visit} from "graphql"; + +const STRING_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_like", label: "contains"}, + {value: "_nlike", label: "does not contain"}, + {value: "_ilike", label: "contains case-insensitive"}, + {value: "_nilike", label: "does not contain case-insensitive"} +]; +const NUMBER_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_gt", label: "greater than"}, + {value: "_lt", label: "less than"}, + {value: "_gte", label: "greater than or equal"}, + {value: "_lte", label: "less than or equal"} +]; + +export function getOperatorsByType(type = 'string') { + const operators = { + string: STRING_OPERATORS, + number: NUMBER_OPERATORS + }; + return operators[type]; +} + +/* eslint-disable no-loop-func */ + +/** + * Parse a GraphQL query into an AST + * @param query + * @returns {DocumentNode} + */ +export function parseQuery(query) { + return parse(query); +} + +/** + * Print an AST back into a GraphQL query + * @param query + * @returns {string} + */ +export function printQuery(query) { + return print(query); +} +/** + * Apply sorters to the AST + * @param ast + * @param sorters + */ +export function applySorters(ast, sorters) { + sorters.forEach((sorter) => { + const fieldPath = sorter.field.split('.'); + visit(ast, { + OperationDefinition: { + enter(node) { + // Loop through each sorter to apply it + // noinspection DuplicatedCode + + let currentSelection = node; // Start with the root operation + + // Navigate down the field path to the correct location + for (let i = 0; i < fieldPath.length - 1; i++) { + let found = false; + visit(currentSelection, { + Field: { + enter(node) { + if (node.name.value === fieldPath[i]) { + currentSelection = node; // Move down to the next level + found = true; + } + } + } + }); + if (!found) break; // Stop if we can't find the next field in the path + } + + // Apply the sorter at the correct level + if (currentSelection) { + const targetFieldName = fieldPath[fieldPath.length - 1]; + let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by'); + if (!orderByArg) { + orderByArg = { + kind: Kind.ARGUMENT, + name: { kind: Kind.NAME, value: 'order_by' }, + value: { kind: Kind.OBJECT, fields: [] }, + }; + currentSelection.arguments.push(orderByArg); + } + + const sorterField = { + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: targetFieldName }, + value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions + }; + + // Add the new sorter condition + orderByArg.value.fields.push(sorterField); + } + } + } + }); + }); +} + +/** + * Apply filters to the AST + * @param ast + * @param filters + */ +export function applyFilters(ast, filters) { + return visit(ast, { + OperationDefinition: { + enter(node) { + filters.forEach(filter => { + const fieldPath = filter.field.split('.'); + let topLevel = false; + + // Determine if the filter should be applied at the top level + if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { + fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + topLevel = true; + } + + if (topLevel) { + // Construct the filter for a top-level application + const targetFieldName = fieldPath[fieldPath.length - 1]; + const filterValue = { + kind: getGraphQLKind(filter.value), + value: filter.value, + }; + + const nestedFilter = { + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: targetFieldName }, + value: { + kind: Kind.OBJECT, + fields: [{ + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: filter.operator }, + value: filterValue, + }], + }, + }; + + // Find or create the where argument for the top-level field + let whereArg = node.selectionSet.selections + .find(selection => selection.name.value === fieldPath[0]) + ?.arguments.find(arg => arg.name.value === 'where'); + + if (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: { kind: Kind.NAME, value: 'where' }, + value: { kind: Kind.OBJECT, fields: [] }, + }; + const topLevelSelection = node.selectionSet.selections.find(selection => + selection.name.value === fieldPath[0] + ); + if (topLevelSelection) { + topLevelSelection.arguments = topLevelSelection.arguments || []; + topLevelSelection.arguments.push(whereArg); + } + } + + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: path }, + value: { kind: Kind.OBJECT, fields: [] } + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; + }); + currentField.fields.push(nestedFilter); + } else { // Directly under the top level + whereArg.value.fields.push(nestedFilter); + } + } else { + // Initialize a reference to the current selection to traverse down the AST + let currentSelection = node; + let whereArgFound = false; + + // Iterate over the fieldPath, except for the last entry, to navigate the structure + for (let i = 0; i < fieldPath.length - 1; i++) { + const fieldName = fieldPath[i]; + let fieldFound = false; + + // Check if the current selection has a selectionSet and selections + if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { + // Look for the field in the current selection's selections + const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); + if (selection) { + // Move down the AST to the found selection + currentSelection = selection; + fieldFound = true; + } + } + + // If the field was not found in the current path, it's an issue + if (!fieldFound) { + console.error(`Field ${fieldName} not found in the current selection.`); + return; // Exit the loop and function due to error + } + } + + // At this point, currentSelection should be the parent field where the filter needs to be applied + // Check if the 'where' argument already exists in the current selection + const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); + if (whereArg) { + whereArgFound = true; + } else { + // If not found, create a new 'where' argument for the current selection + currentSelection.arguments.push({ + kind: Kind.ARGUMENT, + name: { kind: Kind.NAME, value: 'where' }, + value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter + }); + } + + // Assuming the last entry in fieldPath is the field to apply the filter on + const targetField = fieldPath[fieldPath.length - 1]; + const filterValue = { + kind: getGraphQLKind(filter.value), + value: filter.value, + }; + + // Construct the filter field object + const filterField = { + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: targetField }, + value: { + kind: Kind.OBJECT, + fields: [{ + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: filter.operator }, + value: filterValue, + }], + }, + }; + + // Add the filter field to the 'where' clause of the current selection + if (whereArgFound) { + whereArg.value.fields.push(filterField); + } else { + // If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter + currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField); + } + } + + }); + } + } + }); +} + + + +/** + * Get the GraphQL kind for a value + * @param value + * @returns {Kind|Kind.INT} + */ +function getGraphQLKind(value) { + if (typeof value === 'number') { + return value % 1 === 0 ? Kind.INT : Kind.FLOAT; + } else if (typeof value === 'boolean') { + return Kind.BOOLEAN; + } else if (typeof value === 'string') { + return Kind.STRING; + } + // Extend with more types as needed +} + +/** + * Wrap filters in an 'and' object + * @param ast + * @param filterFields + */ +export function wrapFiltersInAnd(ast, filterFields) { + visit(ast, { + OperationDefinition: { + enter(node) { + node.selectionSet.selections.forEach((selection) => { + let whereArg = selection.arguments.find(arg => arg.name.value === 'where'); + if (filterFields.length > 1) { + const andFilter = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: '_and'}, + value: {kind: Kind.LIST, values: filterFields} + }; + whereArg.value.fields.push(andFilter); + } else if (filterFields.length === 1) { + whereArg.value.fields.push(filterFields[0].fields[0]); + } + }); + } + } + }); +} + +/* eslint-enable no-loop-func */ \ No newline at end of file From eb8e9b10ef5a5ab4179ced1602601b29ed81bb99 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 14 Feb 2024 16:47:25 -0800 Subject: [PATCH 086/131] IO-2631 Update Scheduled Completion on Supp --- .../jobs-available-table.container.jsx | 48 +++++-- .../jobs-detail-header.component.jsx | 4 +- .../jobs-find-modal.component.jsx | 133 +++++++++++++++--- .../jobs-find-modal.container.jsx | 4 + client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + 7 files changed, 165 insertions(+), 30 deletions(-) diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index 12d10baf9..54920ec91 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -6,7 +6,7 @@ import { useQuery, } from "@apollo/client"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Col, notification, Row } from "antd"; +import { Col, Row, notification } from "antd"; import Axios from "axios"; import Dinero from "dinero.js"; import moment from "moment"; @@ -30,8 +30,8 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; -import confirmDialog from "../../utils/asyncConfirm"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import confirmDialog from "../../utils/asyncConfirm"; import CriticalPartsScan from "../../utils/criticalPartsScan"; import AlertComponent from "../alert/alert.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; @@ -73,7 +73,15 @@ export function JobsAvailableContainer({ const [selectedJob, setSelectedJob] = useState(null); const [selectedOwner, setSelectedOwner] = useState(null); - const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle); + const [partsQueueToggle, setPartsQueueToggle] = useState( + bodyshop.md_functionality_toggles.parts_queue_toggle + ); + const [updateSchComp, setSchComp] = useState({ + actual_in: moment(), + checked: false, + scheduled_completion: moment(), + automatic: false, + }); const [insertLoading, setInsertLoading] = useState(false); @@ -197,11 +205,16 @@ export function JobsAvailableContainer({ notification["error"]({ message: t("jobs.errors.creating", { error: err.message }), }); - refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)}); + refetch().catch((e) => { + console.error( + `Something went wrong in jobs available table container - ${ + err.message || "" + }` + ); + }); setInsertLoading(false); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); } - }; //Supplement scenario @@ -225,6 +238,23 @@ export function JobsAvailableContainer({ //IO-539 Check for Parts Rate on PAL for SGI use case. await CheckTaxRates(supp, bodyshop); + if (updateSchComp.checked === true) { + if (updateSchComp.automatic === true) { + const job_hrs = supp.joblines.data.reduce( + (acc, val) => acc + val.mod_lb_hrs, + 0 + ); + const num_days = job_hrs / bodyshop.target_touchtime; + supp.actual_in = updateSchComp.actual_in; + supp.scheduled_completion = moment(updateSchComp.actual_in).add( + num_days, + "days" + ); + } else { + supp.scheduled_completion = updateSchComp.scheduled_completion; + } + } + delete supp.owner; delete supp.vehicle; delete supp.ins_co_nm; @@ -261,9 +291,9 @@ export function JobsAvailableContainer({ }, }); - setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); + setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); - if (CriticalPartsScanning.treatment === "on") { + if (CriticalPartsScanning.treatment === "on") { CriticalPartsScan(updateResult.data.update_jobs.returning[0].id); } if (updateResult.errors) { @@ -367,7 +397,6 @@ export function JobsAvailableContainer({ if (error) return ; - return (
diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index 7c58eda52..40c7aa4db 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -131,12 +131,10 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { ))} )} - - - + {job.special_coverage_policy && ( diff --git a/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx b/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx index 37a88e1ef..7616377f0 100644 --- a/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx +++ b/client/src/components/jobs-find-modal/jobs-find-modal.component.jsx @@ -1,9 +1,11 @@ import { SyncOutlined } from "@ant-design/icons"; -import { Checkbox, Divider, Input, Table, Button } from "antd"; -import React from "react"; +import { Button, Checkbox, Divider, Input, Space, Table } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import PhoneFormatter from "../../utils/PhoneFormatter"; +import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; export default function JobsFindModalComponent({ @@ -16,11 +18,13 @@ export default function JobsFindModalComponent({ jobsListRefetch, partsQueueToggle, setPartsQueueToggle, + updateSchComp, + setSchComp, }) { const { t } = useTranslation(); const [modalSearch, setModalSearch] = modalSearchState; const [importOptions, setImportOptions] = importOptionsState; - + const [checkUTT, setCheckUTT] = useState(false); const columns = [ { title: t("jobs.fields.ro_number"), @@ -142,6 +146,35 @@ export default function JobsFindModalComponent({ if (record) { if (record.id) { setSelectedJob(record.id); + if (record.actual_in && record.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: record.actual_in, + scheduled_completion: record.scheduled_completion, + }); + } else { + if (record.actual_in && !record.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: record.actual_in, + scheduled_completion: moment(), + }); + } + if (!record.actual_in && record.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(record.scheduled_completion), + }); + } + if (!record.actual_in && !record.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(), + }); + } + } return; } } @@ -177,6 +210,35 @@ export default function JobsFindModalComponent({ rowSelection={{ onSelect: (props) => { setSelectedJob(props.id); + if (props.actual_in && props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: props.actual_in, + scheduled_completion: props.scheduled_completion, + }); + } else { + if (props.actual_in && !props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: props.actual_in, + scheduled_completion: moment(), + }); + } + if (!props.actual_in && props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(props.scheduled_completion), + }); + } + if (!props.actual_in && !props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(), + }); + } + } }, type: "radio", selectedRowKeys: [selectedJob], @@ -190,23 +252,58 @@ export default function JobsFindModalComponent({ }} /> - - setImportOptions({ - ...importOptions, - overrideHeaders: e.target.checked, - }) - } - > - {t("jobs.labels.override_header")} - - + + setImportOptions({ + ...importOptions, + overrideHeaders: e.target.checked, + }) + } + > + {t("jobs.labels.override_header")} + + setPartsQueueToggle(e.target.checked)} - > - {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} - + > + {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} + + + setSchComp({ ...updateSchComp, checked: e.target.checked }) + } + > + {t("jobs.labels.update_scheduled_completion")} + + {updateSchComp.checked === true ? ( + <> + {checkUTT === false ? ( + { + setSchComp({ ...updateSchComp, scheduled_completion: e }); + }} + /> + ) : null} + { + setCheckUTT(e.target.checked); + setSchComp({ + ...updateSchComp, + scheduled_completion: null, + automatic: true, + }); + }} + > + {t("jobs.labels.calc_scheuled_completion")} + + + ) : null} + ); } diff --git a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx index 64dc8be9a..e6b4ab25e 100644 --- a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx +++ b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx @@ -26,6 +26,8 @@ export default connect( modalSearchState, partsQueueToggle, setPartsQueueToggle, + updateSchComp, + setSchComp, ...modalProps }) { const { t } = useTranslation(); @@ -95,6 +97,8 @@ export default connect( modalSearchState={modalSearchState} partsQueueToggle={partsQueueToggle} setPartsQueueToggle={setPartsQueueToggle} + updateSchComp={updateSchComp} + setSchComp={setSchComp} /> ) : null} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index af575757c..37244a6b1 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1732,6 +1732,7 @@ "ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ", "calc_repair_days": "Calculated Repair Days", "calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).", + "calc_scheuled_completion": "Calculate Scheduled Completion", "cards": { "customer": "Customer Information", "damage": "Area of Damage", @@ -1891,6 +1892,7 @@ "total_sales": "Total Sales", "totals": "Totals", "unvoidnote": "This Job was unvoided.", + "update_scheduled_completion": "Update Scheduled Completion?", "vehicle_info": "Vehicle", "vehicleassociation": "Vehicle Association", "viewallocations": "View Allocations", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 5e57e0911..aefd39986 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1732,6 +1732,7 @@ "ca_gst_all_if_null": "", "calc_repair_days": "", "calc_repair_days_tt": "", + "calc_scheuled_completion": "", "cards": { "customer": "Información al cliente", "damage": "Área de Daño", @@ -1891,6 +1892,7 @@ "total_sales": "", "totals": "", "unvoidnote": "", + "update_scheduled_completion": "", "vehicle_info": "Vehículo", "vehicleassociation": "", "viewallocations": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 92616ae1c..29a33c94f 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1732,6 +1732,7 @@ "ca_gst_all_if_null": "", "calc_repair_days": "", "calc_repair_days_tt": "", + "calc_scheuled_completion": "", "cards": { "customer": "Informations client", "damage": "Zone de dommages", @@ -1891,6 +1892,7 @@ "total_sales": "", "totals": "", "unvoidnote": "", + "update_scheduled_completion": "", "vehicle_info": "Véhicule", "vehicleassociation": "", "viewallocations": "", From a63572583995779557a8ad1048bdb4821833ee1b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 Feb 2024 10:58:19 -0500 Subject: [PATCH 087/131] - Remove console.log statements Signed-off-by: Dave Richer --- client/src/utils/RenderTemplate.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 9c14eb31c..2abb099d8 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -361,7 +361,6 @@ export const fetchFilterData = async ({name}) => { }; const fetchContextData = async (templateObject, jsrAuth) => { - console.log(' FETCH CONTEXT DATA !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') const bodyshop = store.getState().user.bodyshop; jsreport.headers["FirebaseAuthorization"] = @@ -399,15 +398,15 @@ const fetchContextData = async (templateObject, jsrAuth) => { templateQueryToExecute = atob(generalTemplate.content); } - console.log('Template Object'); - console.dir(templateObject); - console.log('Unmodified Query'); - console.dir(templateQueryToExecute); + // Commented out for future revision debugging + // console.log('Template Object'); + // console.dir(templateObject); + // console.log('Unmodified Query'); + // console.dir(templateQueryToExecute); // We have no template filters or sorters, so we can just execute the query and return the data if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) { - console.log('No filters or sorters'); let contextData = {}; if (templateQueryToExecute) { const {data} = await client.query({ @@ -426,20 +425,19 @@ const fetchContextData = async (templateObject, jsrAuth) => { let filterFields = []; if (templateObject?.filters && templateObject?.filters?.length) { - console.log('Applying filters') applyFilters(ast, templateObject.filters, filterFields); wrapFiltersInAnd(ast, filterFields); } if (templateObject?.sorters && templateObject?.sorters?.length) { - console.log('Applying sorters') applySorters(ast, templateObject.sorters); } const finalQuery = printQuery(ast); - console.log('Modified Query'); - console.log(finalQuery); + // commented out for future revision debugging + // console.log('Modified Query'); + // console.log(finalQuery); let contextData = {}; if (templateQueryToExecute) { From cafc0e562850f2b2baa1d285f7437e2cc112854d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 Feb 2024 11:49:02 -0500 Subject: [PATCH 088/131] - Update GUI and provide loading state Signed-off-by: Dave Richer --- ...center-modal-filters-sorters-component.jsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 98293a607..f544e63d6 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -1,9 +1,10 @@ -import {Button, Card, 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 {fetchFilterData} from "../../utils/RenderTemplate"; import {DeleteFilled} from "@ant-design/icons"; import {useTranslation} from "react-i18next"; import {getOperatorsByType} from "../../utils/graphQLmodifier"; +import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; export default function ReportCenterModalFiltersSortersComponent({form}) { return ( @@ -19,16 +20,19 @@ export default function ReportCenterModalFiltersSortersComponent({form}) { function RenderFilters({templateId, form}) { const [state, setState] = useState(null); const [visible, setVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); const {t} = useTranslation(); useEffect(() => { const fetch = async () => { + setIsLoading(true); const data = await fetchFilterData({name: templateId}); if (data?.success) { setState(data.data); } else { setState(null); } + setIsLoading(false); }; if (templateId) { @@ -37,11 +41,20 @@ function RenderFilters({templateId, form}) { }, [templateId]); - if (!templateId || !state) return null; - return ( - - + // Conditional display of filters and sorters + if (!templateId) return null; + if (isLoading) return ; + if (!state) return null; + // Filters and Sorters data available + return ( +
+ setVisible(e.target.checked)} + children={t('reportcenter.labels.advanced_filters')} + /> {visible && (
{state.filters && state.filters.length > 0 && ( @@ -256,6 +269,6 @@ function RenderFilters({templateId, form}) { )}
)} - +
); } \ No newline at end of file From cfc301570e1bdd3f78b3eb2e3762737b3654adb5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 Feb 2024 11:57:27 -0500 Subject: [PATCH 089/131] - Remove redundant CSS Signed-off-by: Dave Richer --- .../report-center-modal-filters-sorters-component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index f544e63d6..62efefac0 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -50,7 +50,6 @@ function RenderFilters({templateId, form}) { return (
setVisible(e.target.checked)} children={t('reportcenter.labels.advanced_filters')} From 767c219af88553ccf9cb766a9771bac9772297f3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 Feb 2024 11:59:56 -0500 Subject: [PATCH 090/131] - Remove additional console.log Signed-off-by: Dave Richer --- client/src/utils/RenderTemplate.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 2abb099d8..33121f6b0 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -148,7 +148,6 @@ export async function RenderTemplates( templateObjects.forEach((template) => { proms.push( (async () => { - console.log(' RENDER TEMPLATE 2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') let {contextData, useShopSpecificTemplate} = await fetchContextData( template, jsrAuth @@ -294,7 +293,6 @@ export const GenerateDocument = async ( }) ); } else if (sendType === "x") { - console.log("excel"); await RenderTemplate(template, bodyshop, false, true); } else if (sendType === "text") { await RenderTemplate(template, bodyshop, false, false, true); From 9cc0d6175e59c497292ad8077f0a37619442097e Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 15 Feb 2024 21:01:43 -0500 Subject: [PATCH 091/131] - Progress commit Signed-off-by: Dave Richer --- ...center-modal-filters-sorters-component.jsx | 24 ++++++++++++++----- client/src/utils/graphQLmodifier.js | 17 +++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 62efefac0..4e3d0aa5d 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -5,8 +5,20 @@ import {DeleteFilled} from "@ant-design/icons"; import {useTranslation} from "react-i18next"; import {getOperatorsByType} from "../../utils/graphQLmodifier"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; +import {setModalContext} from "../../redux/modals/modals.actions"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors"; -export default function ReportCenterModalFiltersSortersComponent({form}) { +const mapDispatchToProps = (dispatch) => ({}); +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser +}); + +export function ReportCenterModalFiltersSortersComponent({form, bodyshop, currentUser}) { + console.dir(bodyshop, {depth: null}) + console.dir(currentUser, {depth: null}) return ( {() => { @@ -17,6 +29,9 @@ export default function ReportCenterModalFiltersSortersComponent({form}) { ); } +export default connect(mapStateToProps, mapDispatchToProps)(ReportCenterModalFiltersSortersComponent); + + function RenderFilters({templateId, form}) { const [state, setState] = useState(null); const [visible, setVisible] = useState(false); @@ -210,7 +225,7 @@ function RenderFilters({templateId, form}) { state.sorters ? state.sorters.map((f) => ({ value: f.name, - label: t(f.translation), + label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label, })) : [] } @@ -230,10 +245,7 @@ function RenderFilters({templateId, form}) { ]} > + { - form.setFieldsValue({[field.name]: {value: value.toString()}}); - }} - /> - } + } } @@ -245,7 +220,7 @@ function RenderFilters({templateId, form}) { ]} > + } + } + + // Number Input + if (type === "number") { + return {form.setFieldsValue({[field.name]: {value: parseInt(value)}}); + }} + /> + } + + // Default to String Input + return { + form.setFieldsValue({[field.name]: {value: value.toString()}}); + }} + /> +} + +export default connect(mapStateToProps, mapDispatchToProps)(ReportCenterModalValueSelectorComponent); \ No newline at end of file diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 33121f6b0..0a6b16a38 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -9,7 +9,7 @@ import {store} from "../redux/store"; import client from "../utils/GraphQLClient"; import cleanAxios from "./CleanAxios"; import {TemplateList} from "./TemplateConstants"; -import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier"; +import {generateTemplate} from "./graphQLmodifier"; const server = process.env.REACT_APP_REPORTS_SERVER_URL; @@ -278,7 +278,9 @@ export const GenerateDocument = async ( sendType, jobid ) => { + const bodyshop = store.getState().user.bodyshop; + if (sendType === "e") { store.dispatch( setEmailOptions({ @@ -404,7 +406,7 @@ const fetchContextData = async (templateObject, jsrAuth) => { // 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 (!(templateObject?.filters?.length || templateObject?.sorters?.length)) { let contextData = {}; if (templateQueryToExecute) { const {data} = await client.query({ @@ -417,36 +419,11 @@ const fetchContextData = async (templateObject, jsrAuth) => { return {contextData, useShopSpecificTemplate}; } - // Parse the query and apply the filters and sorters - const ast = parseQuery(templateQueryToExecute); - - let filterFields = []; - - if (templateObject?.filters && templateObject?.filters?.length) { - applyFilters(ast, templateObject.filters, filterFields); - wrapFiltersInAnd(ast, filterFields); - } - - if (templateObject?.sorters && templateObject?.sorters?.length) { - applySorters(ast, templateObject.sorters); - } - - const finalQuery = printQuery(ast); - - // commented out for future revision debugging - // console.log('Modified Query'); - // console.log(finalQuery); - - let contextData = {}; - if (templateQueryToExecute) { - const {data} = await client.query({ - query: gql(finalQuery), - variables: {...templateObject.variables}, - }); - contextData = data; - } - - return {contextData, useShopSpecificTemplate}; + return await generateTemplate( + templateQueryToExecute, + templateObject, + useShopSpecificTemplate + ); }; //export const displayTemplateInWindow = (html) => { diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js index c9a93542e..a269edd31 100644 --- a/client/src/utils/graphQLmodifier.js +++ b/client/src/utils/graphQLmodifier.js @@ -1,4 +1,6 @@ import {Kind, parse, print, visit} from "graphql"; +import client from "./GraphQLClient"; +import {gql} from "@apollo/client"; const STRING_OPERATORS = [ {value: "_eq", label: "equals"}, @@ -25,16 +27,23 @@ const ORDER_BY_OPERATORS = [ * Get the available operators for filtering * @returns {[{label: string, value: string},{label: string, value: string}]} */ -export function getOrderByOperators() { +export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } +export function generateReflections(reflector) { + // const type = reflector?.type; + // const name = reflector?.name; + + return []; +} + /** * 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 getOperatorsByType(type = 'string') { +export function getWhereOperatorsByType(type = 'string') { const operators = { string: STRING_OPERATORS, number: NUMBER_OPERATORS @@ -61,6 +70,50 @@ export function parseQuery(query) { export function printQuery(query) { return print(query); } + +/** + * Generate a template based on the query and object + * @param templateQueryToExecute + * @param templateObject + * @param useShopSpecificTemplate + * @returns {Promise<{contextData: {}, useShopSpecificTemplate}>} + */ +export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) { + // Advanced Filtering and Sorting modifications start here + + // Parse the query and apply the filters and sorters + const ast = parseQuery(templateQueryToExecute); + + let filterFields = []; + + if (templateObject?.filters && templateObject?.filters?.length) { + applyFilters(ast, templateObject.filters, filterFields); + wrapFiltersInAnd(ast, filterFields); + } + + if (templateObject?.sorters && templateObject?.sorters?.length) { + applySorters(ast, templateObject.sorters); + } + + const finalQuery = printQuery(ast); + + // commented out for future revision debugging + // console.log('Modified Query'); + // console.log(finalQuery); + + let contextData = {}; + if (templateQueryToExecute) { + const {data} = await client.query({ + query: gql(finalQuery), + variables: {...templateObject.variables}, + }); + contextData = data; + } + + return {contextData, useShopSpecificTemplate}; +} + + /** * Apply sorters to the AST * @param ast @@ -278,8 +331,6 @@ export function applyFilters(ast, filters) { }); } - - /** * Get the GraphQL kind for a value * @param value From 3b8e83d88a88a73be11b1d54d470c39331de0bd0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 16 Feb 2024 12:32:40 -0500 Subject: [PATCH 095/131] - clear stage Signed-off-by: Dave Richer --- ...-center-modal-value-selector.component.jsx | 22 ++++++++++++++++--- client/src/utils/graphQLmodifier.js | 6 ----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx b/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx index a8db289aa..7e94badcc 100644 --- a/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx @@ -1,4 +1,3 @@ -import {generateReflections} from "../../utils/graphQLmodifier"; import {Input, InputNumber, Select} from "antd"; import React from "react"; import {createStructuredSelector} from "reselect"; @@ -11,7 +10,10 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser }); + export function ReportCenterModalValueSelectorComponent ({type, reflector, form, field, bodyshop, currentUser}) { + + // TODO: Remove - used for debugging / development console.log(`Entering ReportCenterModalValueSelectorComponent`); console.log('Type') console.log(type) @@ -22,8 +24,17 @@ export function ReportCenterModalValueSelectorComponent ({type, reflector, for console.log('CurrentUser') console.dir(currentUser, {depth: null}) + function generateReflections(reflector) { + // const type = reflector?.type; + // const name = reflector?.name; + + return []; + } + + // We have a reflector, so we can generate a list of options if (reflector) { const reflections = generateReflections(reflector); + // We have options to display, so return a pre-populated select box if (reflections.length > 0) { return { - form.setFieldsValue({[field.name]: {value: value.toString()}}); + form.setFieldsValue({ + [field.name]: {value: value.toString()} + }); }} /> } diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js index a269edd31..3d7e976f0 100644 --- a/client/src/utils/graphQLmodifier.js +++ b/client/src/utils/graphQLmodifier.js @@ -31,12 +31,6 @@ export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } -export function generateReflections(reflector) { - // const type = reflector?.type; - // const name = reflector?.name; - - return []; -} /** * Get the available operators for filtering From 2d6594cc7361f1f63160fc57e29147f983f0a14c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 16 Feb 2024 11:08:59 -0800 Subject: [PATCH 096/131] IO-2631 Correct for Business Days --- .../jobs-available-table.container.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index 54920ec91..942132852 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -246,10 +246,9 @@ export function JobsAvailableContainer({ ); const num_days = job_hrs / bodyshop.target_touchtime; supp.actual_in = updateSchComp.actual_in; - supp.scheduled_completion = moment(updateSchComp.actual_in).add( - num_days, - "days" - ); + supp.scheduled_completion = moment( + updateSchComp.actual_in + ).businessAdd(num_days, "days"); } else { supp.scheduled_completion = updateSchComp.scheduled_completion; } From 845a84c4c81c18b197edd7372537ff8ae5183ff9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 16 Feb 2024 12:30:12 -0800 Subject: [PATCH 097/131] IO-2631 Correct Import Statement for moment --- .../jobs-available-table/jobs-available-table.container.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index 942132852..bbad59845 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -9,7 +9,7 @@ import { useTreatments } from "@splitsoftware/splitio-react"; import { Col, Row, notification } from "antd"; import Axios from "axios"; import Dinero from "dinero.js"; -import moment from "moment"; +import moment from "moment-business-days"; import queryString from "query-string"; import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; From a7e199932c905d0d6593dbe6b37c082a9b98c67d Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 16 Feb 2024 17:14:11 -0800 Subject: [PATCH 098/131] IO-2578 Scoreboard Entries Modal Correct OK button, add sorting to table, adjust date to only be a date, remove closeable on modal Signed-off-by: Allan Carr --- .../scoreboard-entry-edit.component.jsx | 27 ++++++++---- .../scoreboard-jobs-list.component.jsx | 41 ++++++++++++++----- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx index a8488e9dd..31141b628 100644 --- a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx +++ b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx @@ -1,5 +1,14 @@ import { useMutation } from "@apollo/client"; -import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd"; +import { + Button, + Card, + Dropdown, + Form, + InputNumber, + notification, + Space, +} from "antd"; +import moment from "moment"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries"; @@ -13,6 +22,7 @@ export default function ScoreboardEntryEdit({ entry }) { const handleFinish = async (values) => { setLoading(true); + values.date = moment(values.date).format("YYYY-MM-DD"); const result = await updateScoreboardentry({ variables: { sbId: entry.id, sbInput: values }, }); @@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) { > - - - + + + + ); diff --git a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx index 6b1c13ca3..67267fb3f 100644 --- a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx +++ b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx @@ -1,3 +1,4 @@ +import { SyncOutlined } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { Button, Card, Input, Modal, Space, Table, Typography } from "antd"; import React, { useState } from "react"; @@ -5,12 +6,14 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries"; import { DateFormatter } from "../../utils/DateFormatter"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, dateSort } from "../../utils/sorters"; import AlertComponent from "../alert/alert.component"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component"; import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component"; -import { SyncOutlined } from "@ant-design/icons"; -import {pageLimit} from "../../utils/config"; export default function ScoreboardJobsList({ scoreBoardlist }) { const { t } = useTranslation(); const [state, setState] = useState({ @@ -44,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), render: (text, record) => ( {record.job.ro_number || t("general.labels.na")} @@ -55,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { dataIndex: "owner", key: "owner", ellipsis: true, - + sorter: (a, b) => + alphaSort( + OwnerNameDisplayFunction(a.job), + OwnerNameDisplayFunction(b.job) + ), render: (text, record) => , }, { @@ -63,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { dataIndex: "vehicle", key: "vehicle", ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${ + a.job.v_model_desc || "" + }`, + `${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${ + b.job.v_model_desc || "" + }` + ), render: (text, record) => ( {`${record.job.v_model_yr || ""} ${ record.job.v_make_desc || "" @@ -73,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { title: t("scoreboard.fields.date"), dataIndex: "date", key: "date", + sorter: (a, b) => dateSort(a.date, b.date), render: (text, record) => {record.date}, }, - { - title: t("scoreboard.fields.painthrs"), - dataIndex: "painthrs", - key: "painthrs", - }, { title: t("scoreboard.fields.bodyhrs"), dataIndex: "bodyhrs", key: "bodyhrs", + sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs), + }, + { + title: t("scoreboard.fields.painthrs"), + dataIndex: "painthrs", + key: "painthrs", + sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs), }, { title: t("general.labels.actions"), @@ -104,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { visible={state.visible} destroyOnClose width="80%" + closable={false} cancelButtonProps={{ style: { display: "none" } }} - onCancel={() => + onOk={() => setState((state) => ({ ...state, visible: false, From 83bd485597e00a9303d5ff3a39fe5955b5d64c36 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 Feb 2024 09:19:30 -0800 Subject: [PATCH 099/131] IO-2557 New CC Contract Warnings Signed-off-by: Allan Carr --- .../contract-form/contract-form.component.jsx | 40 +++++++++++++++---- .../courtesy-cars-list.component.jsx | 9 ++++- client/src/graphql/courtesy-car.queries.js | 1 + client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/client/src/components/contract-form/contract-form.component.jsx b/client/src/components/contract-form/contract-form.component.jsx index f41933625..6f0125803 100644 --- a/client/src/components/contract-form/contract-form.component.jsx +++ b/client/src/components/contract-form/contract-form.component.jsx @@ -68,6 +68,30 @@ export default function ContractFormComponent({ )} + {create && ( + p.scheduledreturn !== c.scheduledreturn} + > + {() => { + const insuranceOver = + selectedCar && + selectedCar.insuranceexpires && + moment(selectedCar.insuranceexpires) + .endOf("day") + .isBefore(moment(form.getFieldValue("scheduledreturn"))); + if (insuranceOver) + return ( + + + + {t("contracts.labels.insuranceexpired")} + + + ); + return <>; + }} + + )} {() => { const mileageOver = - selectedCar && - selectedCar.nextservicekm <= form.getFieldValue("kmstart"); - + selectedCar && selectedCar.nextservicekm + ? selectedCar.nextservicekm <= form.getFieldValue("kmstart") + : false; const dueForService = selectedCar && selectedCar.nextservicedate && - moment(selectedCar.nextservicedate).isBefore( - moment(form.getFieldValue("scheduledreturn")) - ); - + moment(selectedCar.nextservicedate) + .endOf("day") + .isSameOrBefore( + moment(form.getFieldValue("scheduledreturn")) + ); if (mileageOver || dueForService) return ( @@ -117,7 +142,6 @@ export default function ContractFormComponent({ ); - return <>; }} diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index 2c992990c..d2c4e262d 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -72,7 +72,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, render: (text, record) => { - const { nextservicedate, nextservicekm, mileage } = record; + const { nextservicedate, nextservicekm, mileage, insuranceexpires } = + record; const mileageOver = nextservicekm ? nextservicekm <= mileage : false; @@ -80,10 +81,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { nextservicedate && moment(nextservicedate).endOf("day").isSameOrBefore(moment()); + const insuranceOver = + insuranceexpires && + moment(insuranceexpires).endOf("day").isBefore(moment()); + return ( {t(record.status)} - {(mileageOver || dueForService) && ( + {(mileageOver || dueForService || insuranceOver) && ( diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index 4f7bcd0ca..2012e952e 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -29,6 +29,7 @@ export const QUERY_AVAILABLE_CC = gql` fleetnumber fuel id + insuranceexpires make mileage model diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 77cdb8091..76737f090 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -747,6 +747,7 @@ "driverinformation": "Driver's Information", "findcontract": "Find Contract", "findermodal": "Contract Finder", + "insuranceexpired": "The courtesy car insurance expires before the car is expected to return.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.", "populatefromjob": "Populate from Job", "rates": "Contract Rates", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 341f86fd7..7527ab6ce 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -747,6 +747,7 @@ "driverinformation": "", "findcontract": "", "findermodal": "", + "insuranceexpired": "", "noteconvertedfrom": "", "populatefromjob": "", "rates": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7cdabf620..dbb74ea69 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -747,6 +747,7 @@ "driverinformation": "", "findcontract": "", "findermodal": "", + "insuranceexpired": "", "noteconvertedfrom": "", "populatefromjob": "", "rates": "", From 06ef2482bae4b8681ba4d261a1ee5fa70b6dcda0 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 Feb 2024 12:53:12 -0800 Subject: [PATCH 100/131] IO-2562 CC Info in Job Block UI Correction Signed-off-by: Allan Carr --- .../jobs-detail-header.component.jsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index 40c7aa4db..70dbc033a 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -123,11 +123,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {job.cccontracts.length > 0 && ( - {job.cccontracts.map((c) => ( - {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`} + {job.cccontracts.map((c, index) => ( + + + {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`} + {index !== job.cccontracts.length - 1 ? "," : null} + + ))} )} From 6b7b34ae798aa98e3edd71ce75a6cfd1ef120294 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 Feb 2024 16:00:59 -0500 Subject: [PATCH 101/131] - Progress Commit, this fills agreed upon functionality Signed-off-by: Dave Richer --- _reference/reportFiltersAndSorters.md | 34 +- ...center-modal-filters-sorters-component.jsx | 521 +++++++++++------- .../report-center-modal-utils.js | 123 +++++ ...-center-modal-value-selector.component.jsx | 64 --- .../report-center-modal.component.jsx | 8 +- client/src/utils/RenderTemplate.js | 2 + 6 files changed, 469 insertions(+), 283 deletions(-) create mode 100644 client/src/components/report-center-modal/report-center-modal-utils.js delete mode 100644 client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index bcaa08ade..b83491dbe 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -3,6 +3,9 @@ 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. +# Special Notes +- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected + ## High level Schema Overview ```javascript @@ -36,6 +39,35 @@ const schema = { Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server. A note on special notation used in the `name` field. +## Reflection +Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. + +``` + { + "name": "jobs.status", + "translation": "jobs.fields.status", + "label": "Status", + "type": "string", + "reflector": { + "type": "internal", + "name": "special.job_statuses" + } + }, +``` + +in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses` + +The following cases are available + +- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'` +- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs` +- `special.categories` - This will reflect the categories `bodyshop.md_categories` +- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`' +- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams` +- `special.employees` - This will reflect the employees `bodyshop.employees` +- `special.first_names` - This will reflect the first names `bodyshop.employees` +- `special.last_names` - This will reflect the last names `bodyshop.employees` +- ### Path without brackets, multi level `"name": "jobs.joblines.mod_lb_hrs",` @@ -71,7 +103,6 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! } ``` - ### Path with brackets,top level `"name": "[jobs].joblines.mod_lb_hrs",` This will produce a where clause at the `jobs` level of the graphQL query. @@ -114,7 +145,6 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! - Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues. - Do not add the ability to filter on things like FK constraints, must like the above example. - ## Sorters - Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. - Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 8882e82af..6d09b2fab 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -1,52 +1,336 @@ -import {Button, Card, Checkbox, Col, Form, Row, Select} from "antd"; -import React, {useEffect, useState} from "react"; +import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd"; +import React, {useCallback, useEffect, useMemo, useState} from "react"; import {fetchFilterData} from "../../utils/RenderTemplate"; import {DeleteFilled} from "@ant-design/icons"; import {useTranslation} from "react-i18next"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; +import {generateInternalReflections} from "./report-center-modal-utils"; -export default function ReportCenterModalFiltersSortersComponent({form}) { + +export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { return ( {() => { const key = form.getFieldValue("key"); - return ; + return ; }} ); } -function RenderFilters({templateId, form}) { +/** + * Filters Section + * @param filters + * @param form + * @param bodyshop + * @returns {JSX.Element} + * @constructor + */ +function FiltersSection({filters, form, bodyshop}) { + const {t} = useTranslation(); + + return ( + + + {(fields, {add, remove, move}) => { + return ( +
+ {fields.map((field, index) => ( + + +
+ + + + } + } + + + + + { + () => { + const name = form.getFieldValue(['filters', field.name, "field"]); + const type = filters.find(f => f.name === name)?.type; + const reflector = filters.find(f => f.name === name)?.reflector; + + return + { + (() => { + const generateReflections = (reflector) => { + if (!reflector) return []; + + const {name} = reflector; + const path = name?.split('.'); + const upperPath = path?.[0]; + const finalPath = path?.slice(1).join('.'); + + return generateInternalReflections({ + bodyshop, + upperPath, + finalPath + }); + }; + + const reflections = reflector ? generateReflections(reflector) : []; + + const fieldPath = [[field.name, "value"]]; + + if (reflections.length > 0) { + return ( + form.setFieldsValue(fieldPath, e.target.value)}/> + ); + })() + } + + } + } + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + ); +} + +/** + * Sorters Section + * @param sorters + * @param form + * @returns {JSX.Element} + * @constructor + */ +function SortersSection({sorters, form}) { + const {t} = useTranslation(); + return ( + + + {(fields, {add, remove, move}) => { + return ( +
+ Sorters + {fields.map((field, index) => ( + + +
+ + trigger.parentNode} + /> + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + ); +} + +/** + * Render Filters + * @param templateId + * @param form + * @param bodyshop + * @returns {JSX.Element|null} + * @constructor + */ +function RenderFilters({templateId, form, bodyshop}) { const [state, setState] = useState(null); const [visible, setVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const {t} = useTranslation(); - useEffect(() => { - const fetch = async () => { - setIsLoading(true); - const data = await fetchFilterData({name: templateId}); - if (data?.success) { - setState(data.data); - } else { - setState(null); - } - setIsLoading(false); - }; + const fetch = useCallback(async () => { + // Reset all the filters and Sorters. + form.resetFields(['filters']); + form.resetFields(['sorters']); + setIsLoading(true); + + const data = await fetchFilterData({name: templateId}); + + if (data?.success) { + setState(data.data); + } else { + setState(null); + } + setIsLoading(false); + }, [templateId, form]); + + useEffect(() => { if (templateId) { fetch(); + } - }, [templateId]); + }, [templateId, fetch]); + const filters = useMemo(() => state?.filters || [], [state]); + const sorters = useMemo(() => state?.sorters || [], [state]); - // Conditional display of filters and sorters if (!templateId) return null; if (isLoading) return ; if (!state) return null; - // Filters and Sorters data available return (
{visible && (
- {state.filters && state.filters.length > 0 && ( - - - {(fields, {add, remove, move}) => { - return ( -
- {fields.map((field, index) => ( - - -
- - - - } - } - - - - - - { - () => { - const name = form.getFieldValue(['filters', field.name, "field"]); - const type = state.filters.find(f => f.name === name)?.type; - const reflector = state.filters.find(f => f.name === name)?.reflector; - - return - - - } - } - - - - - { - remove(field.name); - }} - /> - - - - ))} - - - - - ); - }} - - - + {filters.length > 0 && ( + )} - {state.sorters && state.sorters.length > 0 && ( - - - {(fields, {add, remove, move}) => { - return ( -
- Sorters - {fields.map((field, index) => ( - - -
- - - - - - - { - remove(field.name); - }} - /> - - - - ))} - - - - - ); - }} - - + {sorters.length > 0 && ( + )} )} diff --git a/client/src/components/report-center-modal/report-center-modal-utils.js b/client/src/components/report-center-modal/report-center-modal-utils.js new file mode 100644 index 000000000..3d94d9e8e --- /dev/null +++ b/client/src/components/report-center-modal/report-center-modal-utils.js @@ -0,0 +1,123 @@ +import {uniqBy} from "lodash"; + +/** + * Get value from path + * @param obj + * @param path + * @returns {*} + */ +const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj); + +/** + * Valid internal reflections + * Note: This is intended for future functionality + * @type {{special: string[], bodyshop: [{name: string, type: string}]}} + */ +const VALID_INTERNAL_REFLECTIONS = { + bodyshop: [ + { + name: 'md_ro_statuses.statuses', + type: 'kv-to-v' + } + ], +}; + +/** + * Generate options + * @param bodyshop + * @param path + * @param labelPath + * @param valuePath + * @returns {{label: *, value: *}[]} + */ +const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => { + const options = getValueFromPath(bodyshop, path); + return uniqBy(Object.values(options).map((value) => ({ + label: value[labelPath], + value: value[valuePath], + })), 'value'); +} + +/** + * Generate special reflections + * @param bodyshop + * @param finalPath + * @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]} + */ +const generateSpecialReflections = (bodyshop, finalPath) => { + switch (finalPath) { + case 'cost_centers': + return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); + // Special case because Categories is an Array, not an Object. + case 'categories': + const catOptions = getValueFromPath(bodyshop, 'md_categories'); + return uniqBy(catOptions.map((value) => ({ + label: value, + value: value, + })), 'value'); + case 'insurance_companies': + return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); + case 'employee_teams': + return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id'); + // Special case because Employees uses a concatenation of first_name and last_name + case 'employees': + const employeesOptions = getValueFromPath(bodyshop, 'employees'); + return uniqBy(Object.values(employeesOptions).map((value) => ({ + label: `${value.first_name} ${value.last_name}`, + value: value.id, + })), 'value'); + case 'last_names': + return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name'); + case 'first_names': + return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name'); + case 'job_statuses': + const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses'); + return Object.values(statusOptions).map((value) => ({ + label: value, + value + })); + default: + console.error('Invalid Special reflection provided by Report Filters'); + return []; + } +} + +/** + * Generate bodyshop reflections + * @param bodyshop + * @param finalPath + * @returns {{label: *, value: *}[]|*[]} + */ +const generateBodyshopReflections = (bodyshop, finalPath) => { + const options = getValueFromPath(bodyshop, finalPath); + const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath); + if (reflectionRenderer?.type === 'kv-to-v') { + return Object.values(options).map((value) => ({ + label: value, + value + })); + } + return []; +} + +/** + * Generate internal reflections based on the path and bodyshop + * @param bodyshop + * @param upperPath + * @param finalPath + * @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]} + */ +const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => { + switch (upperPath) { + case 'special': + return generateSpecialReflections(bodyshop, finalPath); + case 'bodyshop': + return generateBodyshopReflections(bodyshop, finalPath); + default: + return []; + } +}; + +export { + generateInternalReflections, +} \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx b/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx deleted file mode 100644 index 7e94badcc..000000000 --- a/client/src/components/report-center-modal/report-center-modal-value-selector.component.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import {Input, InputNumber, Select} from "antd"; -import React from "react"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors"; -import {connect} from "react-redux"; - -const mapDispatchToProps = (dispatch) => ({}); -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser -}); - - -export function ReportCenterModalValueSelectorComponent ({type, reflector, form, field, bodyshop, currentUser}) { - - // TODO: Remove - used for debugging / development - console.log(`Entering ReportCenterModalValueSelectorComponent`); - console.log('Type') - console.log(type) - console.log('Reflector') - console.dir(reflector, {depth: null}) - console.log('Bodyshop') - console.dir(bodyshop, {depth: null}) - console.log('CurrentUser') - console.dir(currentUser, {depth: null}) - - function generateReflections(reflector) { - // const type = reflector?.type; - // const name = reflector?.name; - - return []; - } - - // We have a reflector, so we can generate a list of options - if (reflector) { - const reflections = generateReflections(reflector); - // We have options to display, so return a pre-populated select box - if (reflections.length > 0) { - return { - form.setFieldsValue({ - [field.name]: {value: value.toString()} - }); - }} - /> -} - -export default connect(mapStateToProps, mapDispatchToProps)(ReportCenterModalValueSelectorComponent); \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index af4ce7ef4..7fad5ddcf 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -16,9 +16,11 @@ import EmployeeSearchSelect from "../employee-search-select/employee-search-sele import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import "./report-center-modal.styles.scss"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; +import {selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - reportCenterModal: selectReportCenter, + reportCenterModal: selectReportCenter, + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -28,7 +30,7 @@ export default connect( mapDispatchToProps )(ReportCenterModalComponent); -export function ReportCenterModalComponent({reportCenterModal}) { +export function ReportCenterModalComponent({reportCenterModal, bodyshop}) { const [form] = Form.useForm(); const [search, setSearch] = useState(""); @@ -181,7 +183,7 @@ export function ReportCenterModalComponent({reportCenterModal}) { ); }} - + {() => { const key = form.getFieldValue("key"); diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 0a6b16a38..b13228024 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -75,6 +75,8 @@ export default async function RenderTemplate( headerpath: `/${bodyshop.imexshopid}/header.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`, bodyshop: bodyshop, + filters: templateObject?.filters, + sorters: templateObject?.sorters, offset: bodyshop.timezone, //dayjs().utcOffset(), }, }; From 33ec18986db7f9cfbef0522c82cb473f6481b155 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 Feb 2024 13:10:57 -0800 Subject: [PATCH 102/131] IO-2556 CC Sort Order Signed-off-by: Allan Carr --- client/src/graphql/courtesy-car.queries.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index 4f7bcd0ca..bf1693ae0 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql` ] status: { _eq: "courtesycars.status.in" } } + order_by: { fleetnumber: asc } ) { color dailycost @@ -57,7 +58,7 @@ export const CHECK_CC_FLEET_NUMBER = gql` `; export const QUERY_ALL_CC = gql` query QUERY_ALL_CC { - courtesycars { + courtesycars(order_by: { fleetnumber: asc }) { color created_at dailycost From 6cfcab81561302f5cd79499fe8da1e3a8e9bf5c5 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 20 Feb 2024 14:52:40 -0800 Subject: [PATCH 103/131] IO-2557 / IO-1019 Update tooltip Signed-off-by: Allan Carr --- .../courtesy-cars-list.component.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index d2c4e262d..e81a71e3e 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -89,7 +89,17 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { {t(record.status)} {(mileageOver || dueForService || insuranceOver) && ( - + )} From 37708a0b591f0e13e8e144f5eefe05cbab44b23c Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 20 Feb 2024 19:07:23 -0500 Subject: [PATCH 104/131] - Default sorts! Signed-off-by: Dave Richer --- _reference/reportFiltersAndSorters.md | 17 ++++++++++ ...center-modal-filters-sorters-component.jsx | 17 +++++++++- .../report-center-modal.component.jsx | 31 ++++++++++++------- client/src/utils/RenderTemplate.js | 5 ++- client/src/utils/graphQLmodifier.js | 2 ++ 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index b83491dbe..1bd950794 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -148,3 +148,20 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ## 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" + } +} +``` \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 6d09b2fab..76ce5bb76 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -304,14 +304,29 @@ function RenderFilters({templateId, form, bodyshop}) { // Reset all the filters and Sorters. form.resetFields(['filters']); form.resetFields(['sorters']); + form.resetFields(['defaultSorters']); setIsLoading(true); const data = await fetchFilterData({name: templateId}); + // We have Success if (data?.success) { + if (data?.data?.sorters && data?.data?.sorters.length > 0) { + const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => { + return { + field: sorter.name, + direction: sorter.default.direction + }; + }).sort((a, b) => a.default.order - b.default.order); + + form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters)); + } + // Set the state setState(data.data); - } else { + } + // Something went wrong fetching filter data + else { setState(null); } setIsLoading(false); diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index 7fad5ddcf..1dcf81481 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -16,7 +16,7 @@ import EmployeeSearchSelect from "../employee-search-select/employee-search-sele import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import "./report-center-modal.styles.scss"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; -import {selectBodyshop } from "../../redux/user/user.selectors"; +import {selectBodyshop} from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ reportCenterModal: selectReportCenter, @@ -66,22 +66,28 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) { const end = values.dates ? values.dates[1] : null; const { id } = values; - await GenerateDocument( - { + const templateConfig = { name: values.key, variables: { - ...(start - ? { start: moment(start).startOf("day").format("YYYY-MM-DD") } - : {}), - ...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}), - ...(start ? { starttz: moment(start).startOf("day") } : {}), - ...(end ? { endtz: moment(end).endOf("day") } : {}), + ...(start + ? {start: moment(start).startOf("day").format("YYYY-MM-DD")} + : {}), + ...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}), + ...(start ? {starttz: moment(start).startOf("day")} : {}), + ...(end ? {endtz: moment(end).endOf("day")} : {}), - ...(id ? { id: id } : {}), + ...(id ? {id: id} : {}), }, filters: values.filters, sorters: values.sorters, - }, + }; + + if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) { + templateConfig.defaultSorters = JSON.parse(values.defaultSorters); + } + + await GenerateDocument( + templateConfig, { to: values.to, subject: Templates[values.key]?.subject, @@ -119,7 +125,8 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) { onChange={(e) => setSearch(e.target.value)} value={search} /> - + } {...cardProps} >
@@ -220,6 +459,10 @@ export const DashboardScheduledInTodayGql = ` alt_transport clm_no jobid: id + joblines(where: {removed: {_eq: false}}) { + mod_lb_hrs + mod_lbr_ty + } ins_co_nm iouparent ownerid diff --git a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx index 0407e3aad..ab1ab086f 100644 --- a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx @@ -3,37 +3,271 @@ import { ExclamationCircleFilled, PauseCircleOutlined, } from "@ant-design/icons"; -import { Card, Space, Table, Tooltip } from "antd"; +import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import moment from "moment"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { TimeFormatter } from "../../../utils/DateFormatter"; +import { onlyUnique } from "../../../utils/arrayHelper"; +import { alphaSort, dateSort } from "../../../utils/sorters"; +import useLocalStorage from "../../../utils/useLocalStorage"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../../owner-name-display/owner-name-display.component"; import DashboardRefreshRequired from "../refresh-required.component"; -import {pageLimit} from "../../../utils/config"; export default function DashboardScheduledOutToday({ data, ...cardProps }) { const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, }); + const [isTVMode_scheduled_out, setIsTVMode_scheduled_out] = useLocalStorage( + "isTVMode_scheduled_out", + false + ); + if (!data) return null; if (!data.scheduled_out_today) return ; data.scheduled_out_today.forEach((item) => { - item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a") + item.joblines_body = item.joblines + ? item.joblines + .filter((l) => l.mod_lbr_ty !== "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; + item.joblines_ref = item.joblines + ? item.joblines + .filter((l) => l.mod_lbr_ty === "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; }); data.scheduled_out_today.sort(function (a, b) { return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); }); - const columns = [ + const TV_fontSize = 18; + const TV_fontWeight = "bold"; + + const tv_columns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: + state.sortedInfo.columnKey === "scheduled_completion" && + state.sortedInfo.order, + render: (text, record) => ( + + {record.scheduled_completion} + + ), + }, { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()} + > + + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + + + ) : ( + + + + ); + }, + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("appointments.fields.alt_transport"), + dataIndex: "alt_transport", + key: "alt_transport", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + render: (text, record) => ( + + {record.alt_transport} + + ), + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + render: (text, record) => ( + + {record.status} + + ), + }, + { + title: t("jobs.fields.lab"), + dataIndex: "joblines_body", + key: "joblines_body", + sorter: (a, b) => a.joblines_body - b.joblines_body, + sortOrder: + state.sortedInfo.columnKey === "joblines_body" && + state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_body} + + ), + }, + { + title: t("jobs.fields.lar"), + dataIndex: "joblines_ref", + key: "joblines_ref", + sorter: (a, b) => a.joblines_ref - b.joblines_ref, + sortOrder: + state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_ref} + + ), + }, + ]; + + const columns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: + state.sortedInfo.columnKey === "scheduled_completion" && + state.sortedInfo.order, + render: (text, record) => ( + {record.scheduled_completion} + ), + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - + + + + ), }, { @@ -104,7 +334,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { ellipsis: true, responsive: ["md"], render: (text, record) => ( - + {record.ownr_ea} ), }, { @@ -112,6 +342,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { dataIndex: "vehicle", key: "vehicle", ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { return record.vehicleid ? ( alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: + state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), }, { title: t("appointments.fields.alt_transport"), dataIndex: "alt_transport", key: "alt_transport", ellipsis: true, - responsive: ["md"], + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], }, ]; @@ -158,20 +423,30 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { return ( + {t("general.labels.tvmode")} + setIsTVMode_scheduled_out(!isTVMode_scheduled_out)} + defaultChecked={isTVMode_scheduled_out} + /> + + } {...cardProps} >
@@ -188,6 +463,10 @@ export const DashboardScheduledOutTodayGql = ` alt_transport clm_no jobid: id + joblines(where: {removed: {_eq: false}}) { + mod_lb_hrs + mod_lbr_ty + } ins_co_nm iouparent ownerid @@ -200,6 +479,7 @@ export const DashboardScheduledOutTodayGql = ` production_vars ro_number scheduled_completion + status suspended v_make_desc v_model_desc diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index d24dd5641..7012d943b 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -275,26 +275,22 @@ const componentList = { h: 2, }, ScheduleInToday: { - label: i18next.t("dashboard.titles.scheduledintoday", { - date: moment().startOf("day").format("MM/DD/YYYY"), - }), + label: i18next.t("dashboard.titles.scheduledintoday"), component: DashboardScheduledInToday, gqlFragment: DashboardScheduledInTodayGql, - minW: 10, + minW: 6, minH: 2, w: 10, - h: 2, + h: 3, }, ScheduleOutToday: { - label: i18next.t("dashboard.titles.scheduledouttoday", { - date: moment().startOf("day").format("MM/DD/YYYY"), - }), + label: i18next.t("dashboard.titles.scheduledouttoday"), component: DashboardScheduledOutToday, gqlFragment: DashboardScheduledOutTodayGql, - minW: 10, + minW: 6, minH: 2, w: 10, - h: 2, + h: 3, }, }; @@ -306,8 +302,7 @@ const createDashboardQuery = (state) => { .map((item, index) => componentList[item.i].gqlFragment || "") .join(""); return gql` - query QUERY_DASHBOARD_DETAILS { - ${componentBasedAdditions || ""} + query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} monthly_sales: jobs(where: {_and: [ { voided: {_eq: false}}, {date_invoiced: {_gte: "${moment() @@ -317,11 +312,11 @@ const createDashboardQuery = (state) => { .endOf("month") .endOf("day") .toISOString()}"}}]}) { - id - ro_number - date_invoiced - job_totals - rate_la1 + id + ro_number + date_invoiced + job_totals + rate_la1 rate_la2 rate_la3 rate_la4 @@ -344,43 +339,42 @@ const createDashboardQuery = (state) => { rate_mapa rate_mash rate_matd - joblines(where: { removed: { _eq: false } }) { + joblines(where: { removed: { _eq: false } }) { id mod_lbr_ty mod_lb_hrs act_price part_qty part_type + } } - } - production_jobs: jobs(where: { inproduction: { _eq: true } }) { + production_jobs: jobs(where: { inproduction: { _eq: true } }) { + id + ro_number + ins_co_nm + job_totals + joblines(where: { removed: { _eq: false } }) { id - ro_number - ins_co_nm - job_totals - joblines(where: { removed: { _eq: false } }) { - id - mod_lbr_ty - mod_lb_hrs - act_price - part_qty - part_type - } - labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } + mod_lbr_ty + mod_lb_hrs + act_price + part_qty + part_type + } + labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs } } - larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } + } + larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs } } } } - `; + }`; }; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 77cdb8091..dcbed3a58 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -874,6 +874,7 @@ "labels": { "bodyhrs": "Body Hrs", "dollarsinproduction": "Dollars in Production", + "phone": "Phone", "prodhrs": "Production Hrs", "refhrs": "Refinish Hrs" }, @@ -889,8 +890,10 @@ "productiondollars": "Total Dollars in Production", "productionhours": "Total Hours in Production", "projectedmonthlysales": "Projected Monthly Sales", - "scheduledintoday": "Sheduled In Today: {{date}}", - "scheduledouttoday": "Sheduled Out Today: {{date}}" + "scheduledindate": "Sheduled In Today: {{date}}", + "scheduledintoday": "Sheduled In Today:", + "scheduledoutdate": "Sheduled Out Today: {{date}}", + "scheduledouttoday": "Sheduled Out Today:" } }, "dms": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 341f86fd7..da6dad9b2 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -874,6 +874,7 @@ "labels": { "bodyhrs": "", "dollarsinproduction": "", + "phone": "", "prodhrs": "", "refhrs": "" }, @@ -889,7 +890,9 @@ "productiondollars": "", "productionhours": "", "projectedmonthlysales": "", + "scheduledindate": "", "scheduledintoday": "", + "scheduledoutdate": "", "scheduledouttoday": "" } }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7cdabf620..85c29f96d 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -874,6 +874,7 @@ "labels": { "bodyhrs": "", "dollarsinproduction": "", + "phone": "", "prodhrs": "", "refhrs": "" }, @@ -889,7 +890,9 @@ "productiondollars": "", "productionhours": "", "projectedmonthlysales": "", + "scheduledindate": "", "scheduledintoday": "", + "scheduledoutdate": "", "scheduledouttoday": "" } }, From 8a01cd9cb0316c7540b4e6cfbf26cbbc20854325 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 21 Feb 2024 16:57:56 -0500 Subject: [PATCH 112/131] - call changes Signed-off-by: Dave Richer --- .../report-center-modal-filters-sorters-component.jsx | 10 +++++----- .../report-center-modal/report-center-modal-utils.js | 4 +--- client/src/translations/en_us/common.json | 5 +++++ client/src/translations/es/common.json | 5 +++++ client/src/translations/fr/common.json | 5 +++++ 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 7255d5c13..470578ba2 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -42,7 +42,7 @@ function FiltersSection({filters, form, bodyshop}) { { } }; -export { - generateInternalReflections, -} \ No newline at end of file +export {generateInternalReflections,} \ No newline at end of file diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 77cdb8091..e6352c193 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2579,6 +2579,11 @@ "advanced_filters_hide": "Hide", "advanced_filters_filters": "Filters", "advanced_filters_sorters": "Sorters", + "advanced_filters_filter_field": "Field", + "advanced_filters_sorter_field": "Field", + "advanced_filters_sorter_direction": "Direction", + "advanced_filters_filter_operator": "Operator", + "advanced_filters_filter_value": "Value", "dates": "Dates", "employee": "Employee", "filterson": "Filters on {{object}}: {{field}}", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 341f86fd7..f335dc37c 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2579,6 +2579,11 @@ "advanced_filters_hide": "", "advanced_filters_filters": "", "advanced_filters_sorters": "", + "advanced_filters_filter_field": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_filter_operator": "", + "advanced_filters_filter_value": "", "dates": "", "employee": "", "filterson": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7cdabf620..ac74a0fd4 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2579,6 +2579,11 @@ "advanced_filters_hide": "", "advanced_filters_filters": "", "advanced_filters_sorters": "", + "advanced_filters_filter_field": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_filter_operator": "", + "advanced_filters_filter_value": "", "dates": "", "employee": "", "filterson": "", From 4d1f40537cc55e1a5612773d3e62b7d95b954c7a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 22 Feb 2024 12:57:48 -0800 Subject: [PATCH 113/131] IO-2640 Change Variable Names and adjust CSS Signed-off-by: Allan Carr --- .../scheduled-in-today.component.jsx | 47 +++++++++---------- .../scheduled-out-today.component.jsx | 42 ++++++++--------- .../dashboard-grid/dashboard-grid.styles.scss | 2 +- client/src/translations/en_us/common.json | 4 +- 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx index 268cb7a1c..3a902950e 100644 --- a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx @@ -23,8 +23,8 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { const [state, setState] = useState({ sortedInfo: {}, }); - const [isTVMode_scheduled_in, setIsTVMode_scheduled_in] = useLocalStorage( - "isTVMode_scheduled_in", + const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage( + "isTvModeScheduledIn", false ); @@ -75,10 +75,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { return new moment(a.start) - new moment(b.start); }); - const TV_fontSize = 18; - const TV_fontWeight = "bold"; + const tvFontSize = 16; + const tvFontWeight = "bold"; - const tv_columns = [ + const tvColumns = [ { title: t("appointments.fields.time"), dataIndex: "start", @@ -88,7 +88,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { sortOrder: state.sortedInfo.columnKey === "start" && state.sortedInfo.order, render: (text, record) => ( - + {record.start} ), @@ -106,7 +106,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { onClick={(e) => e.stopPropagation()} > - + {record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? ( @@ -139,12 +139,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()} > - + ) : ( - + ); @@ -170,18 +170,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()} > - + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ record.v_model_desc || "" }`} ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ); }, }, @@ -208,7 +206,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { .sort((a, b) => alphaSort(a.text, b.text))) || [], render: (text, record) => ( - + {record.alt_transport} ), @@ -223,8 +221,8 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_body} + + {record.joblines_body.toFixed(1)} ), }, @@ -237,8 +235,8 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_ref} + + {record.joblines_ref.toFixed(1)} ), }, @@ -414,7 +412,6 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { const handleTableChange = (sorter) => { setState({ ...state, sortedInfo: sorter }); }; - return ( {t("general.labels.tvmode")} setIsTVMode_scheduled_in(!isTVMode_scheduled_in)} - defaultChecked={isTVMode_scheduled_in} + onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)} + defaultChecked={isTvModeScheduledIn} /> } @@ -435,12 +432,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
diff --git a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx index ab1ab086f..2a702a616 100644 --- a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx @@ -23,8 +23,8 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { const [state, setState] = useState({ sortedInfo: {}, }); - const [isTVMode_scheduled_out, setIsTVMode_scheduled_out] = useLocalStorage( - "isTVMode_scheduled_out", + const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage( + "isTvModeScheduledOut", false ); @@ -48,10 +48,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); }); - const TV_fontSize = 18; - const TV_fontWeight = "bold"; + const tvFontSize = 18; + const tvFontWeight = "bold"; - const tv_columns = [ + const tvColumns = [ { title: t("jobs.fields.scheduled_completion"), dataIndex: "scheduled_completion", @@ -63,7 +63,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, render: (text, record) => ( - + {record.scheduled_completion} ), @@ -81,7 +81,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { onClick={(e) => e.stopPropagation()} > - + {record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? ( @@ -114,12 +114,12 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()} > - + ) : ( - + ); @@ -145,7 +145,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { to={"/manage/vehicles/" + record.vehicleid} onClick={(e) => e.stopPropagation()} > - + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ record.v_model_desc || "" }`} @@ -153,7 +153,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { ) : ( {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ record.v_model_desc || "" }`} @@ -183,7 +183,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { .sort((a, b) => alphaSort(a.text, b.text))) || [], render: (text, record) => ( - + {record.alt_transport} ), @@ -210,7 +210,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { .sort((a, b) => alphaSort(a.text, b.text))) || [], render: (text, record) => ( - + {record.status} ), @@ -225,8 +225,8 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_body} + + {record.joblines_body.toFixed(1)} ), }, @@ -239,8 +239,8 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_ref} + + {record.joblines_ref.toFixed(1)} ), }, @@ -430,8 +430,8 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { {t("general.labels.tvmode")} setIsTVMode_scheduled_out(!isTVMode_scheduled_out)} - defaultChecked={isTVMode_scheduled_out} + onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)} + defaultChecked={isTvModeScheduledOut} /> } @@ -441,12 +441,12 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
diff --git a/client/src/components/dashboard-grid/dashboard-grid.styles.scss b/client/src/components/dashboard-grid/dashboard-grid.styles.scss index 62a3ae72b..140a1e1f3 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.styles.scss +++ b/client/src/components/dashboard-grid/dashboard-grid.styles.scss @@ -128,7 +128,7 @@ height: 100%; width: 100%; .ant-card-body { - height: 80%; + height: calc(100% - 2rem); width: 100%; // // background-color: red; // height: 90%; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index dcbed3a58..bcba064a0 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -891,9 +891,9 @@ "productionhours": "Total Hours in Production", "projectedmonthlysales": "Projected Monthly Sales", "scheduledindate": "Sheduled In Today: {{date}}", - "scheduledintoday": "Sheduled In Today:", + "scheduledintoday": "Sheduled In Today", "scheduledoutdate": "Sheduled Out Today: {{date}}", - "scheduledouttoday": "Sheduled Out Today:" + "scheduledouttoday": "Sheduled Out Today" } }, "dms": { From 3846b7c5fc5617439e1fbac85e69c865d61a5a87 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 23 Feb 2024 13:01:46 -0800 Subject: [PATCH 114/131] IO-2640 Adjust Filters and Sorters for Table Signed-off-by: Allan Carr --- .../scheduled-in-today.component.jsx | 8 ++++++-- .../scheduled-out-today.component.jsx | 16 +++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx index 3a902950e..fdf083a40 100644 --- a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx @@ -22,6 +22,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, + filteredInfo: {}, }); const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage( "isTvModeScheduledIn", @@ -205,6 +206,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { }) .sort((a, b) => alphaSort(a.text, b.text))) || [], + onFilter: (value, record) => value.includes(record.alt_transport), render: (text, record) => ( {record.alt_transport} @@ -406,12 +408,14 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { }) .sort((a, b) => alphaSort(a.text, b.text))) || [], + onFilter: (value, record) => value.includes(record.alt_transport), }, ]; - const handleTableChange = (sorter) => { - setState({ ...state, sortedInfo: sorter }); + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; + return ( ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ); }, }, @@ -182,6 +181,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { }) .sort((a, b) => alphaSort(a.text, b.text))) || [], + onFilter: (value, record) => value.includes(record.alt_transport), render: (text, record) => ( {record.alt_transport} @@ -209,6 +209,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { }) .sort((a, b) => alphaSort(a.text, b.text))) || [], + onFilter: (value, record) => value.includes(record.status), render: (text, record) => ( {record.status} @@ -414,11 +415,12 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { }) .sort((a, b) => alphaSort(a.text, b.text))) || [], + onFilter: (value, record) => value.includes(record.alt_transport), }, ]; - const handleTableChange = (sorter) => { - setState({ ...state, sortedInfo: sorter }); + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; return ( From a88c102b2787c6a7eaae4dc13db6afecd7415662 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 27 Feb 2024 16:04:41 -0800 Subject: [PATCH 115/131] Prettier and Package Update Azura Storage Blob and Trivago Prettier Sort Imports Signed-off-by: Allan Carr --- .prettierrc.js | 16 + _reference/reportFiltersAndSorters.md | 14 +- package-lock.json | 748 +++++++++++++++++++++++++- package.json | 2 + 4 files changed, 772 insertions(+), 8 deletions(-) create mode 100644 .prettierrc.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..a2bd52a34 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,16 @@ +exports.default = { + printWidth: 120, + useTabs: false, + tabWidth: 2, + trailingComma: "es5", + semi: true, + singleQuote: false, + bracketSpacing: true, + arrowParens: "always", + jsxSingleQuote: false, + bracketSameLine: false, + endOfLine: "lf", + importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + importOrderSeparation: true, + importOrderSortSpecifiers: true, +}; diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index 1bd950794..adb2bc1f0 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -3,7 +3,8 @@ 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. -# Special Notes +## 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 @@ -40,9 +41,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th 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", @@ -67,7 +69,7 @@ The following cases are available - `special.employees` - This will reflect the employees `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees` -- + ### Path without brackets, multi level `"name": "jobs.joblines.mod_lb_hrs",` @@ -104,6 +106,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ### Path with brackets,top level + `"name": "[jobs].joblines.mod_lb_hrs",` This will produce a where clause at the `jobs` level of the graphQL query. @@ -138,6 +141,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ## Known Caveats + - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. - The `dates` object is not yet implemented and will be added in a future release. - The type object must be 'string' or 'number' and is case-sensitive. @@ -146,10 +150,12 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! - Do not add the ability to filter on things like FK constraints, must like the above example. ## Sorters + - Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. - Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. ### 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`. @@ -164,4 +170,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! "direction": "asc" } } -``` \ No newline at end of file +``` diff --git a/package-lock.json b/package-lock.json index 889051d11..9ce8710ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/credential-provider-node": "^3.451.0", + "@azure/storage-blob": "^12.17.0", "@opensearch-project/opensearch": "^2.4.0", "aws4": "^1.12.0", "axios": "^1.6.2", @@ -52,6 +53,7 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "concurrently": "^8.2.2", "source-map-explorer": "^2.5.2" }, @@ -695,11 +697,496 @@ "tslib": "^2.3.1" } }, - "node_modules/@babel/parser": { + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.6.0.tgz", + "integrity": "sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@azure/core-http/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz", + "integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.7.0.tgz", + "integrity": "sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", + "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", - "optional": true, + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "devOptional": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -718,6 +1205,97 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -1034,6 +1612,54 @@ "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz", "integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA==" }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", + "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jsdoc/salty": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", @@ -1106,6 +1732,14 @@ "yarn": "^1.22.10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1714,6 +2348,29 @@ "node": ">= 6" } }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.23.2", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1857,6 +2514,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", @@ -1906,6 +2572,14 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3307,6 +3981,14 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -3854,6 +4536,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/google-auth-library": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", @@ -4446,6 +5137,12 @@ "node": "*" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jose": { "version": "4.15.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", @@ -4459,6 +5156,12 @@ "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4540,6 +5243,18 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "optional": true }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-2-csv": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz", @@ -5457,6 +6172,14 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6894,6 +7617,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6949,6 +7681,14 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 3625e8808..4f27f7b17 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/credential-provider-node": "^3.451.0", + "@azure/storage-blob": "^12.17.0", "@opensearch-project/opensearch": "^2.4.0", "aws4": "^1.12.0", "axios": "^1.6.2", @@ -61,6 +62,7 @@ "xmlbuilder2": "^3.1.1" }, "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "concurrently": "^8.2.2", "source-map-explorer": "^2.5.2" } From e80e40bb76913ebcaeb5c80f191dc462f883598c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 27 Feb 2024 16:37:07 -0800 Subject: [PATCH 116/131] IO-2654 Local Storage Filter State for Courtesy Car List Signed-off-by: Allan Carr --- .../courtesy-cars-list.component.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index e81a71e3e..3bd49d6e7 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter"; import { GenerateDocument } from "../../utils/RenderTemplate"; import { TemplateList } from "../../utils/TemplateConstants"; import { alphaSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; + export default function CourtesyCarsList({ loading, courtesycars, refetch }) { const [state, setState] = useState({ sortedInfo: {}, - filteredInfo: { text: "" }, }); const [searchText, setSearchText] = useState(""); + const [filter, setFilter] = useLocalStorage( + "filter_courtesy_cars_list", + null + ); const { t } = useTranslation(); const columns = [ @@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { dataIndex: "status", key: "status", sorter: (a, b) => alphaSort(a.status, b.status), + filteredValue: filter?.status || null, filters: [ { text: t("courtesycars.status.in"), @@ -112,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { dataIndex: "readiness", key: "readiness", sorter: (a, b) => alphaSort(a.readiness, b.readiness), + filteredValue: filter?.readiness || null, filters: [ { text: t("courtesycars.readiness.ready"), @@ -227,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { ]; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); }; const tableData = searchText From a2e0f9fbe7ab8cfd1307e0ab1206a7f3d0c8126f Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 28 Feb 2024 14:01:35 -0800 Subject: [PATCH 117/131] IO-1366 Audit Log for Bill Delete, Job Suspend, Job Void, Correct Saga Signed-off-by: Allan Carr --- .../bill-delete-button.component.jsx | 18 +++++++++++++++++- .../bills-list-table.component.jsx | 4 ++-- .../jobs-detail-header-actions.component.jsx | 10 ++++++++++ .../src/redux/application/application.sagas.js | 2 +- client/src/translations/en_us/common.json | 5 ++++- client/src/translations/es/common.json | 5 ++++- client/src/translations/fr/common.json | 5 ++++- client/src/utils/AuditTrailMappings.js | 4 ++++ 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx index 5d2d154f6..ca3f3822d 100644 --- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx +++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx @@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client"; import { Button, notification, Popconfirm } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { DELETE_BILL } from "../../graphql/bills.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; -export default function BillDeleteButton({ bill, callback }) { +const mapStateToProps = createStructuredSelector({}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton); + +export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) { const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [deleteBill] = useMutation(DELETE_BILL); @@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) { if (!!!result.errors) { notification["success"]({ message: t("bills.successes.deleted") }); + insertAuditTrail({ + jobid: jobid, + operation: AuditTrailMapping.billdeleted(bill.invoice_number), + }); if (callback && typeof callback === "function") callback(bill.id); } else { diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx index 9dea48f71..5f5bd7011 100644 --- a/client/src/components/bills-list-table/bills-list-table.component.jsx +++ b/client/src/components/bills-list-table/bills-list-table.component.jsx @@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter } from "../../utils/DateFormatter"; -import { alphaSort, dateSort } from "../../utils/sorters"; import { TemplateList } from "../../utils/TemplateConstants"; +import { alphaSort, dateSort } from "../../utils/sorters"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; @@ -58,7 +58,7 @@ export function BillsListTableComponent({ )} - + i18n.t("audit_trail.messages.appointmentinsert", { start }), + billdeleted: (invoice_number) => + i18n.t("audit_trail.messages.billdeleted", { invoice_number }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => @@ -51,6 +53,8 @@ const AuditTrailMapping = { jobstatuschange: (status) => i18n.t("audit_trail.messages.jobstatuschange", { status }), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), + jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }), + jobvoid: () => i18n.t("audit_trail.messages.jobvoid"), }; export default AuditTrailMapping; From a45d0bb9f491be7da94b1421b9b7bdccb06325d5 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 29 Feb 2024 14:13:45 -0800 Subject: [PATCH 118/131] IO-1366 Job Exported Audit Trail Signed-off-by: Allan Carr --- .../jobs-close-export-button.component.jsx | 27 ++++++++++++-- .../jobs-export-all-button.component.jsx | 36 ++++++++++++++++--- client/src/pages/dms/dms.container.jsx | 17 +++++++-- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/AuditTrailMappings.js | 1 + 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx index 8bbda03e1..34acb7994 100644 --- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx +++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx @@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import client from "../../utils/GraphQLClient"; const mapStateToProps = createStructuredSelector({ @@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + function updateJobCache(items) { client.cache.modify({ id: "ROOT_QUERY", @@ -40,6 +47,7 @@ export function JobsCloseExportButton({ disabled, setSelectedJobs, refetch, + insertAuditTrail, }) { const history = useHistory(); const { t } = useTranslation(); @@ -181,6 +189,10 @@ export function JobsCloseExportButton({ key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobexported(), + }); updateJobCache( jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) ); @@ -192,12 +204,20 @@ export function JobsCloseExportButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { + if ( + bodyshop.accountingconfig && + bodyshop.accountingconfig.qbo && + successfulTransactions.length > 0 + ) { notification.open({ type: "success", key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobexported(), + }); updateJobCache([ ...new Set( successfulTransactions.map( @@ -227,4 +247,7 @@ export function JobsCloseExportButton({ ); } -export default connect(mapStateToProps, null)(JobsCloseExportButton); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsCloseExportButton); diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx index 3204d7311..2bb338d97 100644 --- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx +++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx @@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import client from "../../utils/GraphQLClient"; const mapStateToProps = createStructuredSelector({ @@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + function updateJobCache(items) { client.cache.modify({ id: "ROOT_QUERY", @@ -41,6 +48,7 @@ export function JobsExportAllButton({ loadingCallback, completedCallback, refetch, + insertAuditTrail, }) { const { t } = useTranslation(); const [updateJob] = useMutation(UPDATE_JOBS); @@ -177,6 +185,12 @@ export function JobsExportAllButton({ key: "jobsuccessexport", message: t("jobs.successes.exported"), }); + jobUpdateResponse.data.update_jobs.returning.forEach((job) => { + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobexported(), + }); + }); updateJobCache( jobUpdateResponse.data.update_jobs.returning.map( (job) => job.id @@ -190,13 +204,17 @@ export function JobsExportAllButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { + if ( + bodyshop.accountingconfig && + bodyshop.accountingconfig.qbo && + successfulTransactions.length > 0 + ) { notification.open({ type: "success", key: "jobsuccessexport", message: t("jobs.successes.exported"), }); - updateJobCache([ + const successfulTransactionsSet = [ ...new Set( successfulTransactions.map( (st) => @@ -207,7 +225,14 @@ export function JobsExportAllButton({ ] ) ), - ]); + ]; + if (successfulTransactionsSet.length > 0) { + insertAuditTrail({ + jobid: successfulTransactionsSet[0], + operation: AuditTrailMapping.jobexported(), + }); + } + updateJobCache(successfulTransactionsSet); } } }) @@ -225,4 +250,7 @@ export function JobsExportAllButton({ ); } -export default connect(mapStateToProps, null)(JobsExportAllButton); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsExportAllButton); diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 77c670e60..9ed0133a9 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow import { auth } from "../../firebase/firebase.utils"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { + insertAuditTrail, setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -38,6 +40,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); @@ -46,7 +50,7 @@ export const socket = SocketIO( process.env.NODE_ENV === "production" ? process.env.REACT_APP_AXIOS_BASE_API_URL : window.location.origin, - // "http://localhost:4000", // for dev testing, + // "http://localhost:4000", // for dev testing, { path: "/ws", withCredentials: true, @@ -57,7 +61,12 @@ export const socket = SocketIO( } ); -export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { +export function DmsContainer({ + bodyshop, + setBreadcrumbs, + setSelectedHeader, + insertAuditTrail, +}) { const { t } = useTranslation(); const [logLevel, setLogLevel] = useState("DEBUG"); const history = useHistory(); @@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { notification.success({ message: t("jobs.successes.exported"), }); + insertAuditTrail({ + jobid: payload, + operation: AuditTrailMapping.jobexported(), + }); history.push("/manage/accounting/receivables"); }); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c7259897e..9edefeaeb 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.", + "jobexported": "Job has been exported.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobimported": "Job imported.", "jobinproductionchange": "Job production status set to {{inproduction}}", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index ed7cc4efa..2a1e1be9b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "", "jobchecklist": "", "jobconverted": "", + "jobexported": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 638b7d0d6..d2cacffeb 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -115,6 +115,7 @@ "jobassignmentremoved": "", "jobchecklist": "", "jobconverted": "", + "jobexported": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index 757bdd43c..4ad405f0a 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -35,6 +35,7 @@ const AuditTrailMapping = { i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobexported: () => i18n.t("audit_trail.messages.jobexported"), jobfieldchange: (field, value) => i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), jobimported: () => i18n.t("audit_trail.messages.jobimported"), From a4a84572b71b426183936e3b20170245fb8bd162 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 29 Feb 2024 15:36:58 -0800 Subject: [PATCH 119/131] IO-2656 Job Count on Scoreboard Jobs Signed-off-by: Allan Carr --- .../scoreboard-day-stats.component.jsx | 11 +++-- .../scoreboard-targets-table.component.jsx | 40 +++++++++++++++++-- client/src/translations/en_us/common.json | 3 ++ client/src/translations/es/common.json | 3 ++ client/src/translations/fr/common.json | 3 ++ 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx index cfd6d945d..ecaf189fb 100644 --- a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx +++ b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx @@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) { return acc + value.bodyhrs; }, 0); + const numJobs = entries.length; + return ( bodyHrs ? "red" : "green" }} - label="B" + label="Body" value={bodyHrs.toFixed(1)} /> paintHrs ? "red" : "green" }} - label="P" + label="Refinish" value={paintHrs.toFixed(1)} /> - - + + + ); } diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx index 112d441f6..11129b025 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx @@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { let ret = { todayBody: 0, todayPaint: 0, + todayJobs: 0, weeklyPaint: 0, + weeklyJobs: 0, weeklyBody: 0, toDateBody: 0, toDatePaint: 0, + toDateJobs: 0, }; const today = moment(); @@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[today.format("YYYY-MM-DD")].forEach((d) => { ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayPaint = ret.todayPaint + d.painthrs; + ret.todayJobs++; }); } @@ -49,6 +53,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => { ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs; + ret.weeklyJobs++; }); } StartOfWeek = StartOfWeek.add(1, "day"); @@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDatePaint = ret.toDatePaint + d.painthrs; + ret.toDateJobs++; }); } startOfMonth = startOfMonth.add(1, "day"); @@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { @@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { @@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 501c016ed..8c320540d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "All Employee Time Tickets", "asoftodaytarget": "As of Today", "body": "Body", + "bodyabbrev": "B", "bodycharttitle": "Body Targets vs Actual", "calendarperiod": "Periods based on calendar weeks/months.", "combinedcharttitle": "Combined Targets vs Actual", @@ -2774,6 +2775,7 @@ "productivestatistics": "Productive Hours Statistics", "productivetimeticketsoverdate": "Productive Hours over Selected Dates", "refinish": "Refinish", + "refinishabbrev": "R", "refinishcharttitle": "Refinish Targets vs Actual", "targets": "Targets", "thismonth": "This Month", @@ -2781,6 +2783,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "total": "Total", "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index c9b8d5b63..5152382a9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "", "asoftodaytarget": "", "body": "", + "bodyabbrev": "", "bodycharttitle": "", "calendarperiod": "", "combinedcharttitle": "", @@ -2774,6 +2775,7 @@ "productivestatistics": "", "productivetimeticketsoverdate": "", "refinish": "", + "refinishabbrev": "", "refinishcharttitle": "", "targets": "", "thismonth": "", @@ -2781,6 +2783,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "total": "", "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e2f4705e8..5443f5142 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2758,6 +2758,7 @@ "allemployeetimetickets": "", "asoftodaytarget": "", "body": "", + "bodyabbrev": "", "bodycharttitle": "", "calendarperiod": "", "combinedcharttitle": "", @@ -2774,6 +2775,7 @@ "productivestatistics": "", "productivetimeticketsoverdate": "", "refinish": "", + "refinishabbrev": "", "refinishcharttitle": "", "targets": "", "thismonth": "", @@ -2781,6 +2783,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "total": "", "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", From 0529ac4478a7381d3244637feb4a47f80017519d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 29 Feb 2024 22:22:55 -0500 Subject: [PATCH 120/131] - Reports V3 Targeted at Master Signed-off-by: Dave Richer --- _reference/reportFiltersAndSorters.md | 31 +- ...center-modal-filters-sorters-component.jsx | 82 +++- .../report-center-modal-utils.js | 28 +- client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + client/src/utils/graphQLmodifier.js | 447 +++++++++++------- 7 files changed, 394 insertions(+), 200 deletions(-) diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md index 1bd950794..8aa65abb9 100644 --- a/_reference/reportFiltersAndSorters.md +++ b/_reference/reportFiltersAndSorters.md @@ -3,6 +3,12 @@ This documentation details the schema required for `.filters` files on the report server. It is used to dynamically modify the graphQL query and provide the user more power over their reports. +For filters and sorters, valid types include (`type` key in the schema): +- string (default) +- number +- bool or boolean +- date + # Special Notes - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected @@ -67,7 +73,9 @@ The following cases are available - `special.employees` - This will reflect the employees `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees` -- +- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources +- `special.class`- This will reflect the class `bodyshop.md_classes` +- ### Path without brackets, multi level `"name": "jobs.joblines.mod_lb_hrs",` @@ -104,6 +112,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ### Path with brackets,top level + `"name": "[jobs].joblines.mod_lb_hrs",` This will produce a where clause at the `jobs` level of the graphQL query. @@ -138,16 +147,22 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! ``` ## Known Caveats -- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. -- The `dates` object is not yet implemented and will be added in a future release. -- The type object must be 'string' or 'number' and is case-sensitive. + +- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` + is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. +- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive. - The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. -- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues. +- Do not add the ability to filter things that are already filtered as part of the original query, this would be + redundant and could cause issues. - Do not add the ability to filter on things like FK constraints, must like the above example. ## Sorters -- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. -- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters. + +- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, + a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` + would be added at the `joblines` level. +- Most of the reports currently do sorting on a template level, this will need to change to actually see the results + using the sorters. ### Default Sorters - A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves. @@ -164,4 +179,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz! "direction": "asc" } } -``` \ No newline at end of file +``` diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 470578ba2..892ce67d5 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import {generateInternalReflections} from "./report-center-modal-utils"; - +import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx"; export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { return ( @@ -33,7 +33,7 @@ function FiltersSection({filters, form, bodyshop}) { return ( - {(fields, {add, remove, move}) => { + {(fields, {add, remove}) => { return (
{fields.map((field, index) => ( @@ -70,7 +70,9 @@ function FiltersSection({filters, form, bodyshop}) {
- + { () => { const name = form.getFieldValue(['filters', field.name, "field"]); @@ -80,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) { key={`${index}operator`} label={t('reportcenter.labels.advanced_filters_filter_operator')} name={[field.name, "operator"]} - dependencies={[]} rules={[ { required: true, @@ -90,19 +91,32 @@ function FiltersSection({filters, form, bodyshop}) { > trigger.parentNode} + onChange={(value) => { + form.setFieldValue(fieldPath, value); + }} + /> + ); + } return ( trigger.parentNode} + options={[ + { + label: t('reportcenter.labels.advanced_filters_true'), + value: true + }, + { + label: t('reportcenter.labels.advanced_filters_false'), + value: false + } + ]} + onChange={(value) => form.setFieldValue(fieldPath, value)} + /> + ); + } return ( form.setFieldValue(fieldPath, e.target.value)}/> + disabled={!operator} + onChange={(e) => form.setFieldValue(fieldPath, e.target.value)} + /> ); })() } @@ -203,12 +265,12 @@ function FiltersSection({filters, form, bodyshop}) { * @returns {JSX.Element} * @constructor */ -function SortersSection({sorters, form}) { +function SortersSection({sorters}) { const {t} = useTranslation(); return ( - {(fields, {add, remove, move}) => { + {(fields, {add, remove}) => { return (
Sorters diff --git a/client/src/components/report-center-modal/report-center-modal-utils.js b/client/src/components/report-center-modal/report-center-modal-utils.js index 2f9fc5e87..97b693a61 100644 --- a/client/src/components/report-center-modal/report-center-modal-utils.js +++ b/client/src/components/report-center-modal/report-center-modal-utils.js @@ -8,6 +8,21 @@ import {uniqBy} from "lodash"; */ 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 @@ -46,15 +61,16 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => { */ 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': - const catOptions = getValueFromPath(bodyshop, 'md_categories'); - return uniqBy(catOptions.map((value) => ({ - label: value, - value: value, - })), 'value'); + return generateOptionsFromArray(bodyshop, 'md_categories'); case 'insurance_companies': return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); case 'employee_teams': @@ -118,4 +134,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => { } }; -export {generateInternalReflections,} \ No newline at end of file +export {generateInternalReflections} \ No newline at end of file diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 501c016ed..b1f0af354 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2585,6 +2585,8 @@ "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", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index c9b8d5b63..9cca30f97 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2585,6 +2585,8 @@ "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": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e2f4705e8..e234ffac6 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2585,6 +2585,8 @@ "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": "", diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js index 3d4079874..d31c6584a 100644 --- a/client/src/utils/graphQLmodifier.js +++ b/client/src/utils/graphQLmodifier.js @@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql"; import client from "./GraphQLClient"; import {gql} from "@apollo/client"; +/* eslint-disable no-loop-func */ + +/** + * The available operators for filtering (string) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ const STRING_OPERATORS = [ {value: "_eq", label: "equals"}, {value: "_neq", label: "does not equal"}, {value: "_like", label: "contains"}, {value: "_nlike", label: "does not contain"}, {value: "_ilike", label: "contains case-insensitive"}, - {value: "_nilike", label: "does not contain case-insensitive"} + {value: "_nilike", label: "does not contain case-insensitive"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} ]; + +/** + * The available operators for filtering (dates) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ +const DATE_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_gt", label: "greater than"}, + {value: "_lt", label: "less than"}, + {value: "_gte", label: "greater than or equal"}, + {value: "_lte", label: "less than or equal"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} +]; + +/** + * The available operators for filtering (booleans) + * @type {[{label: string, value: string},{label: string, value: string}]} + */ +const BOOLEAN_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, +]; + +/** + * The available operators for filtering (numbers) + * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} + */ const NUMBER_OPERATORS = [ {value: "_eq", label: "equals"}, {value: "_neq", label: "does not equal"}, {value: "_gt", label: "greater than"}, {value: "_lt", label: "less than"}, {value: "_gte", label: "greater than or equal"}, - {value: "_lte", label: "less than or equal"} + {value: "_lte", label: "less than or equal"}, + {value: "_in", label: "in", type: "array"}, + {value: "_nin", label: "not in", type: "array"} ]; + +/** + * 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"} @@ -31,7 +75,6 @@ export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } - /** * Get the available operators for filtering * @param type @@ -40,13 +83,14 @@ export function getOrderOperatorsByType() { export function getWhereOperatorsByType(type = 'string') { const operators = { string: STRING_OPERATORS, - number: NUMBER_OPERATORS + number: NUMBER_OPERATORS, + boolean: BOOLEAN_OPERATORS, + bool: BOOLEAN_OPERATORS, + date: DATE_OPERATORS }; return operators[type]; } -/* eslint-disable no-loop-func */ - /** * Parse a GraphQL query into an AST * @param query @@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u // Parse the query and apply the filters and sorters const ast = parseQuery(templateQueryToExecute); - let filterFields = []; if (templateObject?.filters && templateObject?.filters?.length) { - applyFilters(ast, templateObject.filters, filterFields); - wrapFiltersInAnd(ast, filterFields); + applyFilters(ast, templateObject.filters); } if (templateObject?.sorters && templateObject?.sorters?.length) { @@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u return {contextData, useShopSpecificTemplate}; } - /** * Apply sorters to the AST * @param ast @@ -149,16 +190,16 @@ export function applySorters(ast, sorters) { if (!orderByArg) { orderByArg = { kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'order_by' }, - value: { kind: Kind.OBJECT, fields: [] }, + name: {kind: Kind.NAME, value: 'order_by'}, + value: {kind: Kind.OBJECT, fields: []}, }; currentSelection.arguments.push(orderByArg); } const sorterField = { kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetFieldName }, - value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions + name: {kind: Kind.NAME, value: targetFieldName}, + value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions }; // Add the new sorter condition @@ -170,10 +211,59 @@ export function applySorters(ast, sorters) { }); } +/** + * Apply Top Level Sub to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyTopLevelSub(node, fieldPath, filterField) { + // Find or create the where argument for the top-level subfield + let whereArg = node.selectionSet.selections + .find(selection => selection.name.value === fieldPath[0]) + ?.arguments.find(arg => arg.name.value === 'where'); + + if (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []}, + }; + const topLevelSubSelection = node.selectionSet.selections.find(selection => + selection.name.value === fieldPath[0] + ); + if (topLevelSubSelection) { + topLevelSubSelection.arguments = topLevelSubSelection.arguments || []; + topLevelSubSelection.arguments.push(whereArg); + } + } + + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: path}, + value: {kind: Kind.OBJECT, fields: []} + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; + }); + currentField.fields.push(filterField); + } else { // Directly under the top level + whereArg.value.fields.push(filterField); + } +} + /** * Apply filters to the AST * @param ast * @param filters + * @returns {ASTNode} */ export function applyFilters(ast, filters) { return visit(ast, { @@ -182,192 +272,197 @@ export function applyFilters(ast, filters) { filters.forEach(filter => { const fieldPath = filter.field.split('.'); let topLevel = false; + let topLevelSub = false; // Determine if the filter should be applied at the top level - if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { - fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + if (fieldPath.length === 2) { topLevel = true; } - if (topLevel) { - // Construct the filter for a top-level application - const targetFieldName = fieldPath[fieldPath.length - 1]; - const filterValue = { - kind: getGraphQLKind(filter.value), - value: filter.value, - }; - - const nestedFilter = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetFieldName }, - value: { - kind: Kind.OBJECT, - fields: [{ - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: filter.operator }, - value: filterValue, - }], - }, - }; - - // Find or create the where argument for the top-level field - let whereArg = node.selectionSet.selections - .find(selection => selection.name.value === fieldPath[0]) - ?.arguments.find(arg => arg.name.value === 'where'); - - if (!whereArg) { - whereArg = { - kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'where' }, - value: { kind: Kind.OBJECT, fields: [] }, - }; - const topLevelSelection = node.selectionSet.selections.find(selection => - selection.name.value === fieldPath[0] - ); - if (topLevelSelection) { - topLevelSelection.arguments = topLevelSelection.arguments || []; - topLevelSelection.arguments.push(whereArg); - } - } - - // Correctly position the nested filter without an extra 'where' - if (fieldPath.length > 2) { // More than one level deep - let currentField = whereArg.value; - fieldPath.slice(1, -1).forEach((path, index) => { - let existingField = currentField.fields.find(f => f.name.value === path); - if (!existingField) { - existingField = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: path }, - value: { kind: Kind.OBJECT, fields: [] } - }; - currentField.fields.push(existingField); - } - currentField = existingField.value; - }); - currentField.fields.push(nestedFilter); - } else { // Directly under the top level - whereArg.value.fields.push(nestedFilter); - } - } else { - // Initialize a reference to the current selection to traverse down the AST - let currentSelection = node; - let whereArgFound = false; - - // Iterate over the fieldPath, except for the last entry, to navigate the structure - for (let i = 0; i < fieldPath.length - 1; i++) { - const fieldName = fieldPath[i]; - let fieldFound = false; - - // Check if the current selection has a selectionSet and selections - if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { - // Look for the field in the current selection's selections - const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); - if (selection) { - // Move down the AST to the found selection - currentSelection = selection; - fieldFound = true; - } - } - - // If the field was not found in the current path, it's an issue - if (!fieldFound) { - console.error(`Field ${fieldName} not found in the current selection.`); - return; // Exit the loop and function due to error - } - } - - // At this point, currentSelection should be the parent field where the filter needs to be applied - // Check if the 'where' argument already exists in the current selection - const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); - if (whereArg) { - whereArgFound = true; - } else { - // If not found, create a new 'where' argument for the current selection - currentSelection.arguments.push({ - kind: Kind.ARGUMENT, - name: { kind: Kind.NAME, value: 'where' }, - value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter - }); - } - - // Assuming the last entry in fieldPath is the field to apply the filter on - const targetField = fieldPath[fieldPath.length - 1]; - const filterValue = { - kind: getGraphQLKind(filter.value), - value: filter.value, - }; - - // Construct the filter field object - const filterField = { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: targetField }, - value: { - kind: Kind.OBJECT, - fields: [{ - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: filter.operator }, - value: filterValue, - }], - }, - }; - - // Add the filter field to the 'where' clause of the current selection - if (whereArgFound) { - whereArg.value.fields.push(filterField); - } else { - // If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter - currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField); - } + if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { + fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + topLevelSub = true; } + // Construct the filter for a top-level application + const targetFieldName = fieldPath[fieldPath.length - 1]; + + let filterValue = createFilterValue(filter); + let filterField = createFilterField(targetFieldName, filter, filterValue); + + if (topLevel) { + applyTopLevelFilter(node, fieldPath, filterField); + } else if (topLevelSub) { + applyTopLevelSub(node, fieldPath, filterField); + } else { + applyNestedFilter(node, fieldPath, filterField); + } }); } } }); } +/** + * Create a filter value based on the filter + * @param filter + * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}} + */ +function createFilterValue(filter) { + if (Array.isArray(filter.value)) { + // If it's an array, create a list value with the array items + return { + kind: Kind.LIST, + values: filter.value.map(item => ({ + kind: getGraphQLKind(item), + value: item, + })), + }; + } else { + // If it's not an array, use the existing logic + return { + kind: getGraphQLKind(filter.value), + value: filter.value, + }; + } +} + +/** + * Create a filter field based on the target field and filter + * @param targetFieldName + * @param filter + * @param filterValue + * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}} + */ +function createFilterField(targetFieldName, filter, filterValue) { + return { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: targetFieldName}, + value: { + kind: Kind.OBJECT, + fields: [{ + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: filter.operator}, + value: filterValue, + }], + }, + }; +} + +/** + * Apply a top-level filter to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyTopLevelFilter(node, fieldPath, filterField) { + // Find or create the where argument for the top-level field + let whereArg = node.selectionSet.selections + .find(selection => selection.name.value === fieldPath[0]) + ?.arguments.find(arg => arg.name.value === 'where'); + + if (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []}, + }; + const topLevelSelection = node.selectionSet.selections.find(selection => + selection.name.value === fieldPath[0] + ); + if (topLevelSelection) { + topLevelSelection.arguments = topLevelSelection.arguments || []; + topLevelSelection.arguments.push(whereArg); + } + } + + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: path}, + value: {kind: Kind.OBJECT, fields: []} + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; + }); + currentField.fields.push(filterField); + } else { // Directly under the top level + whereArg.value.fields.push(filterField); + } +} + +/** + * Apply a nested filter to the AST + * @param node + * @param fieldPath + * @param filterField + */ +function applyNestedFilter(node, fieldPath, filterField) { + // Initialize a reference to the current selection to traverse down the AST + let currentSelection = node; + + // Iterate over the fieldPath, except for the last entry, to navigate the structure + for (let i = 0; i < fieldPath.length - 1; i++) { + const fieldName = fieldPath[i]; + let fieldFound = false; + + // Check if the current selection has a selectionSet and selections + if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { + // Look for the field in the current selection's selections + const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); + if (selection) { + // Move down the AST to the found selection + currentSelection = selection; + fieldFound = true; + } + } + + // If the field was not found in the current path, it's an issue + if (!fieldFound) { + console.error(`Field ${fieldName} not found in the current selection.`); + return; // Exit the loop and function due to error + } + } + + // At this point, currentSelection should be the parent field where the filter needs to be applied + // Check if the 'where' argument already exists in the current selection + const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); + if (!whereArg) { + // If not found, create a new 'where' argument for the current selection + currentSelection.arguments.push({ + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter + }); + } + + // Add the filter field to the 'where' clause of the current selection + currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField); +} + /** * Get the GraphQL kind for a value * @param value * @returns {Kind|Kind.INT} */ function getGraphQLKind(value) { - if (typeof value === 'number') { + if (Array.isArray(value)) { + return Kind.LIST; + } else if (typeof value === 'number') { return value % 1 === 0 ? Kind.INT : Kind.FLOAT; } else if (typeof value === 'boolean') { return Kind.BOOLEAN; } else if (typeof value === 'string') { return Kind.STRING; + } else if (value instanceof Date) { + return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string } - // Extend with more types as needed } -/** - * Wrap filters in an 'and' object - * @param ast - * @param filterFields - */ -export function wrapFiltersInAnd(ast, filterFields) { - visit(ast, { - OperationDefinition: { - enter(node) { - node.selectionSet.selections.forEach((selection) => { - let whereArg = selection.arguments.find(arg => arg.name.value === 'where'); - if (filterFields.length > 1) { - const andFilter = { - kind: Kind.OBJECT_FIELD, - name: {kind: Kind.NAME, value: '_and'}, - value: {kind: Kind.LIST, values: filterFields} - }; - whereArg.value.fields.push(andFilter); - } else if (filterFields.length === 1) { - whereArg.value.fields.push(filterFields[0].fields[0]); - } - }); - } - } - }); -} - -/* eslint-enable no-loop-func */ \ No newline at end of file +/* eslint-enable no-loop-func */ From 85a3aeb33534a0e585c005a66387d51f80c29c20 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 1 Mar 2024 11:51:01 -0800 Subject: [PATCH 121/131] Resolve refund payment logging. --- .../payment-expanded-row/payment-expanded-row.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx index a150eee78..57e81cf77 100644 --- a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx +++ b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx @@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => { await insertPayment({ variables: { paymentInput: { - amount: -refund_response.data.amount, - transactionid: payment_response.response.receiptelements.transid, + amount: -refund_response?.data?.amount, + transactionid: payment_response?.response?.receiptelements?.transid, payer: record.payer, type: "Refund", jobid: payment_response.jobid, From 959f7780e842a43bb687be2f3229492b580aaaa4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 4 Mar 2024 12:13:41 -0800 Subject: [PATCH 122/131] IO-2660 Phonebook Drawer Title Signed-off-by: Allan Carr --- .../phonebook-form.component.jsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/client/src/components/phonebook-form/phonebook-form.component.jsx b/client/src/components/phonebook-form/phonebook-form.component.jsx index e40096ce9..4795c14ad 100644 --- a/client/src/components/phonebook-form/phonebook-form.component.jsx +++ b/client/src/components/phonebook-form/phonebook-form.component.jsx @@ -1,6 +1,12 @@ import { Button, Form, Input, PageHeader, Space } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectAuthLevel, + selectBodyshop, +} from "../../redux/user/user.selectors"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; import PhoneFormItem, { @@ -8,12 +14,6 @@ import PhoneFormItem, { } from "../form-items-formatted/phone-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { - selectAuthLevel, - selectBodyshop, -} from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ authLevel: selectAuthLevel, bodyshop: selectBodyshop, @@ -46,13 +46,19 @@ export function PhonebookFormComponent({ return (
+ {() => + `${form.getFieldValue("firstname") || ""} ${ + form.getFieldValue("lastname") || "" + }${ + form.getFieldValue("company") + ? ` - ${form.getFieldValue("company")}` + : "" + }` + } + + } extra={