From e15edeadb5c594da3ebc5885cad302bf1cba61a8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 29 Nov 2023 23:54:37 +0000 Subject: [PATCH 001/139] 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/139] 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/139] 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/139] 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 b97de32a44d46dc1fa90ee35315b5090b161ee32 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 12 Dec 2023 15:41:36 -0800 Subject: [PATCH 005/139] IO-2501 Add Jobs Complete Not Invoiced Section to Stats --- .../scoreboard-timetickets.component.jsx | 19 ++++++++- ...scoreboard-timetickets.stats.component.jsx | 42 +++++++++++++++++-- client/src/graphql/timetickets.queries.js | 31 +++++++++++++- client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + 6 files changed, 90 insertions(+), 8 deletions(-) diff --git a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx index 0117279d5..af4f28695 100644 --- a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx @@ -29,7 +29,7 @@ export default connect( export function ScoreboardTimeTicketsStats({ bodyshop }) { const { t } = useTranslation(); - const startDate = moment().startOf("month") + const startDate = moment().startOf("month"); const endDate = moment().endOf("month"); const fixedPeriods = useMemo(() => { @@ -84,6 +84,8 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -340,11 +342,21 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { larData.push({ ...r, ...lar }); }); + const jobData = {}; + data.jobs.forEach((job) => { + job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0); + }); + jobData.tthrs = data.jobs + .reduce((acc, val) => acc + val.tthrs, 0) + .toFixed(1); + jobData.count = data.jobs.length.toFixed(0); + return { fixed: ret, combinedData: combinedData, labData: labData, larData: larData, + jobData: jobData, }; }, [fixedPeriods, data, bodyshop]); @@ -356,7 +368,10 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { - + {/* This Month */} - + @@ -482,7 +482,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Last Month */} - + @@ -556,7 +556,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Efficiency Over Period */} - + + + + + + + + + + + + {t("scoreboard.labels.totalhrs")} + + } + value={jobData.tthrs} + valueStyle={{ + fontSize: statisticSize, + fontWeight: statisticWeight, + }} + /> + + + + {/* Disclaimer */} diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index bb90c4e9d..9f3dec512 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -143,9 +143,14 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` $end: date! $fixedStart: date! $fixedEnd: date! + $jobStart: timestamptz! + $jobEnd: timestamptz! ) { timetickets( - where: { date: { _gte: $start, _lte: $end }, cost_center: {_neq: "timetickets.labels.shift"} } + where: { + date: { _gte: $start, _lte: $end } + cost_center: { _neq: "timetickets.labels.shift" } + } order_by: { date: desc_nulls_first } ) { actualhrs @@ -176,7 +181,10 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` } } fixedperiod: timetickets( - where: { date: { _gte: $fixedStart, _lte: $fixedEnd }, cost_center: {_neq: "timetickets.labels.shift"} } + where: { + date: { _gte: $fixedStart, _lte: $fixedEnd } + cost_center: { _neq: "timetickets.labels.shift" } + } order_by: { date: desc_nulls_first } ) { actualhrs @@ -205,6 +213,25 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` last_name } } + jobs( + where: { + date_invoiced: { _is_null: true } + ro_number: { _is_null: false } + voided: { _eq: false } + _or: [ + { actual_completion: { _gte: $jobStart, _lte: $jobEnd } } + { actual_delivery: { _gte: $jobStart, _lte: $jobEnd } } + ] + } + ) { + id + joblines(order_by: { line_no: asc }, where: { removed: { _eq: false } }) { + convertedtolbr + convertedtolbr_data + mod_lb_hrs + mod_lbr_ty + } + } } `; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index bbac4cc7b..04a647924 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2695,6 +2695,7 @@ "efficiencyoverperiod": "Efficiency over Selected Dates", "entries": "Scoreboard Entries", "jobs": "Jobs", + "jobscompletednotinvoiced": "Completed Not Invoiced", "lastmonth": "Last Month", "lastweek": "Last Week", "monthlytarget": "Monthly", @@ -2709,6 +2710,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", "weeklytarget": "Weekly", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 33cf621da..56ca50899 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2695,6 +2695,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2710,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 3b5251fa7..690bf246b 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2695,6 +2695,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2710,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", From 2dd56590d3f3d799c0b567b6bcc876dbfcf80e3f Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 14 Dec 2023 08:57:36 -0800 Subject: [PATCH 006/139] Admin panel to force email addresses to be lowercase to conform with firebase --- server/firebase/firebase-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 7221649ec..e203bcbf3 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -50,7 +50,7 @@ exports.createUser = async (req, res) => { `, { user: { - email, + email: email.toLowerCase(), authid: userRecord.uid, associations: { data: [{ shopid, authlevel, active: true }], From 661bedbe5b555540a69246f480091a2e715670bf Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 18 Dec 2023 12:36:46 -0800 Subject: [PATCH 007/139] IO-2506 Federal Tax Exempt on Bill Entry Will Toggle Federal Tax off on any new line or retroactively toggle it off on all lines when switch is enabled. Limited to PBS or CDK setups. --- .../bill-form/bill-form.component.jsx | 25 +++++++++++++++++-- .../bill-form/bill-form.lines.component.jsx | 8 +++--- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 44b8cd815..579d11b7b 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -79,6 +79,18 @@ export function BillFormComponent({ }); }; + const handleFederalTaxExemptSwitchToggle = (checked) => { + if (checked) { + const values = form.getFieldsValue("billlines"); + if (values && values.billlines && values.billlines.length > 0) { + values.billlines.forEach((b) => { + b.applicable_taxes.federal = false; + }); + } + form.setFieldsValue({ billlines: values.billlines }); + } + }; + useEffect(() => { if (job) form.validateFields(["is_credit_memo"]); }, [job, form]); @@ -387,7 +399,16 @@ export function BillFormComponent({ > - + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + + + + ) : null} + {() => { const values = form.getFieldsValue([ "billlines", @@ -405,7 +426,7 @@ export function BillFormComponent({ totals = CalculateBillTotal(values); if (!!totals) return ( -
+
Date: Mon, 18 Dec 2023 14:12:21 -0800 Subject: [PATCH 008/139] IO-2509 Report Center RBAC --- .../components/rbac-wrapper/rbac-defaults.js | 5 +- .../report-center-modal.container.jsx | 5 +- .../shop-info/shop-info.rbac.component.jsx | 428 +++++++++--------- 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, 230 insertions(+), 211 deletions(-) diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js index 24a564872..a01dd54c2 100644 --- a/client/src/components/rbac-wrapper/rbac-defaults.js +++ b/client/src/components/rbac-wrapper/rbac-defaults.js @@ -55,10 +55,11 @@ const ret = { "shiftclock:view": 2, "shop:config": 4, - "shop:rbac": 5, - "shop:vendors": 2, "shop:dashboard": 3, + "shop:rbac": 5, + "shop:reportcenter": 2, "shop:templates": 4, + "shop:vendors": 2, "temporarydocs:view": 2, diff --git a/client/src/components/report-center-modal/report-center-modal.container.jsx b/client/src/components/report-center-modal/report-center-modal.container.jsx index f0d361785..84fe65560 100644 --- a/client/src/components/report-center-modal/report-center-modal.container.jsx +++ b/client/src/components/report-center-modal/report-center-modal.container.jsx @@ -5,6 +5,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; +import RbacWrapperComponent from "../rbac-wrapper/rbac-wrapper.component"; import ReportCenterModalComponent from "./report-center-modal.component"; const mapStateToProps = createStructuredSelector({ @@ -33,7 +34,9 @@ export function ReportCenterModalContainer({ destroyOnClose width="80%" > - + + + ); } diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index fe4f80f31..e4152fb88 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -28,18 +28,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { return ( - - - + + + + + + + + + + + + @@ -173,26 +209,38 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { + + + @@ -208,30 +256,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - - - - - - - @@ -280,6 +292,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > + + + + + + + + + + + + + + + @@ -329,74 +401,14 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { - - - - - - - - - - - - - - - @@ -412,18 +424,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + - - - - - - @@ -556,18 +556,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + + + + + + + + + + - - - - - - - - - {Simple_Inventory.treatment === "on" && ( <> Config", "dashboard": "Shop -> Dashboard", "rbac": "Shop -> RBAC", + "reportcenter": "Shop -> Report Center", "templates": "Shop -> Templates", "vendors": "Shop -> Vendors" }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 33cf621da..1d3784879 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -447,6 +447,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 3b5251fa7..abdb28e00 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -447,6 +447,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, From 0117237988a02fffb23da2e4f0021844cd2acd66 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 19 Dec 2023 09:18:00 -0800 Subject: [PATCH 009/139] IO-2506 Correct for variable immutibility and nested ifs --- .../bill-form/bill-form.component.jsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 579d11b7b..ba9f937ba 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -80,15 +80,17 @@ export function BillFormComponent({ }; const handleFederalTaxExemptSwitchToggle = (checked) => { - if (checked) { - const values = form.getFieldsValue("billlines"); - if (values && values.billlines && values.billlines.length > 0) { - values.billlines.forEach((b) => { - b.applicable_taxes.federal = false; - }); - } - form.setFieldsValue({ billlines: values.billlines }); - } + // Early gate + if (!checked) return; + const values = form.getFieldsValue("billlines"); + // Gate bill lines + if (!values?.billlines?.length) return; + + const billlines = values.billlines.map((b) => { + b.applicable_taxes.federal = false; + return b; + }); + form.setFieldsValue({ billlines }); }; useEffect(() => { From 25e8eaa1d40e0b1aa2af0472f63811308bfc849c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 21 Dec 2023 09:24:25 -0800 Subject: [PATCH 010/139] IO-2505 Conversation List Print --- .../chat-conversation-title.component.jsx | 2 + .../chat-print-button.component.jsx | 59 +++++++++++++++++++ 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/TemplateConstants.js | 11 ++++ 6 files changed, 81 insertions(+) create mode 100644 client/src/components/chat-print-button/chat-print-button.component.jsx diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 1503bf46f..110ea2ea3 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -4,6 +4,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component"; import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component"; import ChatLabelComponent from "../chat-label/chat-label.component"; +import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; export default function ChatConversationTitle({ conversation }) { @@ -13,6 +14,7 @@ export default function ChatConversationTitle({ conversation }) { {conversation && conversation.phone_num} + ({ + setEmailOptions: (e) => dispatch(setEmailOptions(e)), +}); + +export function ChatPrintButton({ conversation }) { + const [loading, setLoading] = useState(false); + + return ( + + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "p", + conversation.id + ); + setLoading(false); + }} + /> + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "e", + conversation.id + ); + setLoading(false); + }} + /> + {loading && } + + ); +} +export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index bbac4cc7b..4d54c3b95 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2044,6 +2044,9 @@ "sentby": "Sent by {{by}} at {{time}}", "typeamessage": "Send a message...", "unarchive": "Unarchive" + }, + "render": { + "conversation_list": "Conversation List" } }, "notes": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 33cf621da..6e558f3f9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2044,6 +2044,9 @@ "sentby": "", "typeamessage": "Enviar un mensaje...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 3b5251fa7..7874bc07e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2044,6 +2044,9 @@ "sentby": "", "typeamessage": "Envoyer un message...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index c5e10d712..365b2431e 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2102,6 +2102,17 @@ export const TemplateList = (type, context) => { // }, } : {}), + ...(!type || type === "messaging" + ? { + conversation_list: { + title: i18n.t("messaging.render.conversation_list"), + description: "", + subject: i18n.t("messaging.render.conversation_list"), + key: "conversation_list", + disabled: false, + }, + } + : {}), ...(!type || type === "vendor" ? { purchases_by_vendor_detailed: { From 1e7f43fe3df295e57ce57eafb9cbeb4371eb42b6 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 21 Dec 2023 18:32:37 -0800 Subject: [PATCH 011/139] IO-2501 Correct for missing query variables --- .../scoreboard-timetickets/scoreboard-timetickets.component.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx index 0fc84af6d..9fdb384d1 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -65,6 +65,8 @@ export default function ScoreboardTimeTickets() { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", From f6a59bdf5512d53321aa6b9163f6f20eee1ff6f2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 26 Dec 2023 11:18:39 -0800 Subject: [PATCH 012/139] IO-2511 Bill Label Reprint --- .../bill-detail-edit-component.jsx | 5 ++- .../bill-print-button.component.jsx | 38 +++++++++++++++++++ client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 client/src/components/bill-print-button/bill-print-button.component.jsx diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index e92bb175b..12ca80301 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect"; import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, - UPDATE_BILL_LINE + UPDATE_BILL_LINE, } from "../../graphql/bill-lines.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; @@ -20,6 +20,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AlertComponent from "../alert/alert.component"; import BillFormContainer from "../bill-form/bill-form.container"; import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; +import BillPrintButton from "../bill-print-button/bill-print-button.component"; import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; @@ -176,7 +177,7 @@ export function BillDetailEditcontainer({ extra={ - + form.submit()} diff --git a/client/src/components/bill-print-button/bill-print-button.component.jsx b/client/src/components/bill-print-button/bill-print-button.component.jsx new file mode 100644 index 000000000..46e7d2f72 --- /dev/null +++ b/client/src/components/bill-print-button/bill-print-button.component.jsx @@ -0,0 +1,38 @@ +import { Button, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; + +export default function BillPrintButton({ billid }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const Templates = TemplateList("job_special"); + + const submitHandler = async () => { + setLoading(true); + try { + await GenerateDocument( + { + name: Templates.parts_invoice_label_single.key, + variables: { + id: billid, + }, + }, + {}, + "p" + ); + } catch (e) { + console.warn("Warning: Error generating a document."); + } + setLoading(false); + }; + + return ( + + + + ); +} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index b96b555db..48ea29334 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -214,6 +214,7 @@ "new": "New Bill", "noneselected": "No bill selected.", "onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.", + "printlabels": "Print Labels", "retailtotal": "Bills Retail Total", "savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.", "state_tax": "Provincial/State Tax", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f68cf4a41..5e99b800a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -214,6 +214,7 @@ "new": "", "noneselected": "", "onlycmforinvoiced": "", + "printlabels": "", "retailtotal": "", "savewithdiscrepancy": "", "state_tax": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index d0e44b704..0c7089e6c 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -214,6 +214,7 @@ "new": "", "noneselected": "", "onlycmforinvoiced": "", + "printlabels": "", "retailtotal": "", "savewithdiscrepancy": "", "state_tax": "", From 2daee84fbf196e5888b87cd0eb443c614e81800d Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 27 Dec 2023 11:07:17 -0800 Subject: [PATCH 013/139] IO-2512 Date Esimated on Manual Created Jobs --- client/src/pages/jobs-create/jobs-create.container.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 15af524ed..dfe83358c 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -1,6 +1,6 @@ -import _ from "lodash"; import { useLazyQuery, useMutation } from "@apollo/client"; import { Form, notification } from "antd"; +import _ from "lodash"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -90,6 +90,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { {}, values, { date_open: new Date() }, + { date_estimated: new Date() }, { vehicle: state.vehicle.selectedid || state.vehicle.none From 87a01208fbce2d53e1f041fee8c3616111ade9dc Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 27 Dec 2023 13:35:29 -0800 Subject: [PATCH 014/139] IO-2500 Readiness and Fuel Level --- .../contract-cars/contract-cars.component.jsx | 9 ++ .../courtesy-car-form.component.jsx | 7 ++ ...ourtesy-car-readiness-select.component.jsx | 35 ++++++ .../courtesy-cars-list.component.jsx | 50 +++++++++ client/src/graphql/courtesy-car.queries.js | 33 +++--- .../courtesy-car-detail.page.container.jsx | 17 +-- client/src/translations/en_us/common.json | 5 + client/src/translations/es/common.json | 5 + client/src/translations/fr/common.json | 5 + hasura/metadata/tables.yaml | 105 +++++++++--------- .../down.sql | 4 + .../up.sql | 2 + 12 files changed, 203 insertions(+), 74 deletions(-) create mode 100644 client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx create mode 100644 hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql create mode 100644 hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql diff --git a/client/src/components/contract-cars/contract-cars.component.jsx b/client/src/components/contract-cars/contract-cars.component.jsx index 541d6d7c3..6162ae7af 100644 --- a/client/src/components/contract-cars/contract-cars.component.jsx +++ b/client/src/components/contract-cars/contract-cars.component.jsx @@ -35,6 +35,15 @@ export default function ContractsCarsComponent({ state.sortedInfo.columnKey === "status" && state.sortedInfo.order, render: (text, record) =>
{t(record.status)}
, }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + sortOrder: + state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness), + }, { title: t("courtesycars.fields.year"), dataIndex: "year", diff --git a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx index d5315ed3a..a64cb8a2a 100644 --- a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx +++ b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries"; import { DateFormatter } from "../../utils/DateFormatter"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; +import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; //import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; @@ -213,6 +214,12 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { >
+ + +
{ + const [option, setOption] = useState(value); + const { t } = useTranslation(); + + useEffect(() => { + if (value !== option && onChange) { + onChange(option); + } + }, [value, option, onChange]); + + return ( + + ); +}; +export default forwardRef(CourtesyCarReadinessComponent); 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 ab85f95bc..7c4b25534 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 @@ -91,6 +91,26 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { ); }, }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + filters: [ + { + text: t("courtesycars.readiness.ready"), + value: "courtesycars.readiness.ready", + }, + { + text: t("courtesycars.readiness.notready"), + value: "courtesycars.readiness.notready", + }, + ], + onFilter: (value, record) => value.includes(record.readiness), + sortOrder: + state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness), + }, { title: t("courtesycars.fields.year"), dataIndex: "year", @@ -131,6 +151,36 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order, }, + { + title: t("courtesycars.fields.fuel"), + dataIndex: "fuel", + key: "fuel", + sorter: (a, b) => alphaSort(a.fuel, b.fuel), + sortOrder: + state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order, + render: (text, record) => { + switch (record.fuel) { + case 100: + return t("courtesycars.labels.fuel.full"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 63: + return t("courtesycars.labels.fuel.58"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 38: + return t("courtesycars.labels.fuel.34"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 0: + return t("courtesycars.labels.fuel.empty"); + default: + return record.fuel; + } + }, + }, { title: t("courtesycars.labels.outwith"), dataIndex: "outwith", diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index a5bcbea55..4f7bcd0ca 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -30,15 +30,15 @@ export const QUERY_AVAILABLE_CC = gql` fuel id make - model - plate - status - year - dailycost mileage + model notes nextservicekm nextservicedate + plate + readiness + status + year } } `; @@ -68,19 +68,20 @@ export const QUERY_ALL_CC = gql` insuranceexpires leaseenddate make + mileage model nextservicedate nextservicekm notes plate purchasedate + readiness registrationexpires serviceenddate servicestartdate status vin year - mileage cccontracts( where: { status: { _eq: "contracts.status.out" } } order_by: { contract_date: desc } @@ -90,10 +91,10 @@ export const QUERY_ALL_CC = gql` scheduledreturn job { id - ro_number ownr_fn ownr_ln ownr_co_nm + ro_number } } } @@ -119,19 +120,20 @@ export const QUERY_CC_BY_PK = gql` insuranceexpires leaseenddate make + mileage model nextservicedate nextservicekm notes plate purchasedate + readiness registrationexpires serviceenddate servicestartdate status vin year - mileage cccontracts_aggregate { aggregate { count(distinct: true) @@ -139,21 +141,20 @@ export const QUERY_CC_BY_PK = gql` } cccontracts(offset: $offset, limit: $limit, order_by: $order) { agreementnumber + driver_fn + driver_ln id - status - start - scheduledreturn kmstart kmend - driver_ln - driver_fn + scheduledreturn + start + status job { - ro_number - + id ownr_ln ownr_fn ownr_co_nm - id + ro_number } } } 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..bf73b4cc3 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 @@ -1,11 +1,14 @@ import { useMutation, useQuery } from "@apollo/client"; import { Form, notification } from "antd"; import moment from "moment"; +import queryString from "query-string"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { useParams } from "react-router-dom"; +import { useLocation, useParams } from "react-router-dom"; import AlertComponent from "../../components/alert/alert.component"; +import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; +import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { QUERY_CC_BY_PK, UPDATE_CC } from "../../graphql/courtesy-car.queries"; import { @@ -13,13 +16,10 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import { pageLimit } from "../../utils/config"; import { CreateRecentItem } from "../../utils/create-recent-item"; +import UndefinedToNull from "./../../utils/undefinedtonull"; import CourtesyCarDetailPageComponent from "./courtesy-car-detail.page.component"; -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)), @@ -112,7 +112,10 @@ export function CourtesyCarDetailPageContainer({ setSaveLoading(true); const result = await updateCourtesyCar({ - variables: { cc: { ...values }, ccId: ccId }, + variables: { + cc: { ...UndefinedToNull(values, ["readiness"]) }, + ccId: ccId, + }, refetchQueries: ["QUERY_CC_BY_PK"], awaitRefetchQueries: true, }); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index b96b555db..8410ea08b 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -785,6 +785,7 @@ "notes": "Notes", "plate": "Plate Number", "purchasedate": "Purchase Date", + "readiness": "Readiness", "registrationexpires": "Registration Expires On", "serviceenddate": "Usage End Date", "servicestartdate": "Usage Start Date", @@ -821,6 +822,10 @@ }, "successes": { "saved": "Courtesy Car saved successfully." + }, + "readiness": { + "notready": "Not Ready", + "ready": "Ready" } }, "csi": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f68cf4a41..c759e1ffa 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -785,6 +785,7 @@ "notes": "", "plate": "", "purchasedate": "", + "readiness": "", "registrationexpires": "", "serviceenddate": "", "servicestartdate": "", @@ -821,6 +822,10 @@ }, "successes": { "saved": "" + }, + "readiness": { + "notready": "", + "ready": "" } }, "csi": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index d0e44b704..90941e460 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -785,6 +785,7 @@ "notes": "", "plate": "", "purchasedate": "", + "readiness": "", "registrationexpires": "", "serviceenddate": "", "servicestartdate": "", @@ -821,6 +822,10 @@ }, "successes": { "saved": "" + }, + "readiness": { + "notready": "", + "ready": "" } }, "csi": { diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index d94a97af0..216ac8c48 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -1388,60 +1388,62 @@ - active: _eq: true columns: - - id - - created_at - - updated_at - bodyshopid - - make - - model - - year - - plate - color - - vin - - fleetnumber - - purchasedate - - servicestartdate - - serviceenddate - - leaseenddate - - status - - nextservicekm - - nextservicedate - - damage - - notes - - fuel - - registrationexpires - - insuranceexpires + - created_at - dailycost + - damage + - fleetnumber + - fuel + - id + - insuranceexpires + - leaseenddate + - make - mileage + - model + - nextservicedate + - nextservicekm + - notes + - plate + - purchasedate + - readiness + - registrationexpires + - serviceenddate + - servicestartdate + - status + - updated_at + - vin + - year select_permissions: - role: user permission: columns: + - bodyshopid + - color + - created_at + - dailycost + - damage + - fleetnumber + - fuel + - id - insuranceexpires - leaseenddate + - make + - mileage + - model - nextservicedate + - nextservicekm + - notes + - plate - purchasedate + - readiness - registrationexpires - serviceenddate - servicestartdate - - dailycost - - fuel - - mileage - - nextservicekm - - color - - damage - - fleetnumber - - make - - model - - notes - - plate - status + - updated_at - vin - year - - created_at - - updated_at - - bodyshopid - - id filter: bodyshop: associations: @@ -1456,31 +1458,32 @@ - role: user permission: columns: + - bodyshopid + - color + - created_at + - dailycost + - damage + - fleetnumber + - fuel + - id - insuranceexpires - leaseenddate + - make + - mileage + - model - nextservicedate + - nextservicekm + - notes + - plate - purchasedate + - readiness - registrationexpires - serviceenddate - servicestartdate - - dailycost - - fuel - - mileage - - nextservicekm - - color - - damage - - fleetnumber - - make - - model - - notes - - plate - status + - updated_at - vin - year - - created_at - - updated_at - - bodyshopid - - id filter: bodyshop: associations: diff --git a/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql new file mode 100644 index 000000000..5e741dcc7 --- /dev/null +++ b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."courtesycars" add column "readiness" text +-- null; diff --git a/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql new file mode 100644 index 000000000..4f5339ede --- /dev/null +++ b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql @@ -0,0 +1,2 @@ +alter table "public"."courtesycars" add column "readiness" text + null; From bdb2951330277cc2babc3ba6a79d69858f52395e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 27 Dec 2023 13:55:31 -0800 Subject: [PATCH 015/139] IO-2500 Courtesy Car Readiness --- .../courtesy-car-fuel-select.component.jsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx index 2f9226b83..6b561d12a 100644 --- a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx +++ b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx @@ -34,6 +34,32 @@ const CourtesyCarFuelComponent = (props, ref) => { step={null} style={{ marginLeft: "2rem", marginRight: "2rem" }} {...props} + tooltip={{ + formatter: (value) => { + switch (value) { + case 0: + return t("courtesycars.labels.fuel.empty"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 38: + return t("courtesycars.labels.fuel.38"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 63: + return t("courtesycars.labels.fuel.58"); + case 75: + return t("courtesycars.labels.fuel.34"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 100: + return t("courtesycars.labels.fuel.full"); + default: + return value; + } + }, + }} /> ); }; From b0d077e104a4c23587ce7ed4528b7493927125b9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 28 Dec 2023 10:12:59 -0800 Subject: [PATCH 016/139] IO-2514 Production Board Estimators --- .../production-list-columns.data.js | 29 ++++++++++++++++++- client/src/graphql/jobs.queries.js | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) 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 a93e0f3e6..a7f1ead6e 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 @@ -1,4 +1,4 @@ -import { PauseCircleOutlined, BranchesOutlined } from "@ant-design/icons"; +import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons"; import { Space, Tooltip } from "antd"; import i18n from "i18next"; import moment from "moment"; @@ -536,6 +536,33 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { ), }, + { + title: i18n.t("jobs.labels.estimator"), + dataIndex: "estimator", + key: "estimator", + sorter: (a, b) => + alphaSort( + `${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(), + `${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim() + ), + sortOrder: + state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order, + filters: + (bodyshop && + bodyshop.md_estimators.map((s) => { + return { + text: `${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim(), + value: [`${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim()], + }; + })) || + [], + 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(), + }, //Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client. // { diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index b18673de6..fbeb8740d 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -364,6 +364,8 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` employee_refinish employee_prep employee_csr + est_ct_fn + est_ct_ln suspended date_repairstarted joblines_status { From e236d6e9123117f045b009e72276eff622c84633 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 28 Dec 2023 10:57:03 -0800 Subject: [PATCH 017/139] IO-2489 Registration # in Vehicle Info Box --- .../jobs-detail-header/jobs-detail-header.component.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 a5dd99f6f..30bb98560 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 @@ -1,8 +1,8 @@ import { + BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled, - BranchesOutlined, } from "@ant-design/icons"; import { Card, Col, Row, Space, Tag, Tooltip } from "antd"; import React, { useState } from "react"; @@ -222,6 +222,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {`${job.v_vin || t("general.labels.na")}`} + + {job.regie_number || t("general.labels.na")} + From 823f07409aca7584a804c3118cf3e695209b58df Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 29 Dec 2023 16:03:40 -0800 Subject: [PATCH 018/139] IO-2517 All Courtesy Car Warning Indicator --- .../courtesy-cars-list/courtesy-cars-list.component.jsx | 4 ++-- 1 file changed, 2 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 7c4b25534..3be08fff2 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 @@ -74,10 +74,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { render: (text, record) => { const { nextservicedate, nextservicekm, mileage } = record; - const mileageOver = nextservicekm <= mileage; + const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const dueForService = - nextservicedate && moment(nextservicedate).isBefore(moment()); + nextservicedate && moment(nextservicedate).isSameOrBefore(moment()); return ( From bfe94e3068c06d9b256269439ae275f2c6cb3f90 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 29 Dec 2023 16:17:37 -0800 Subject: [PATCH 019/139] IO-2517 Add same check within C/C form --- .../courtesy-car-form/courtesy-car-form.component.jsx | 10 ++++------ .../courtesy-cars-list.component.jsx | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx index a64cb8a2a..f9a32dfee 100644 --- a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx +++ b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx @@ -214,10 +214,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { > - +
@@ -234,8 +231,9 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { > {() => { const nextservicekm = form.getFieldValue("nextservicekm"); - const mileageOver = - nextservicekm && nextservicekm <= form.getFieldValue("mileage"); + const mileageOver = nextservicekm + ? nextservicekm <= form.getFieldValue("mileage") + : false; if (mileageOver) 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 3be08fff2..2c992990c 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 @@ -77,7 +77,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const dueForService = - nextservicedate && moment(nextservicedate).isSameOrBefore(moment()); + nextservicedate && + moment(nextservicedate).endOf("day").isSameOrBefore(moment()); return ( From ded798fdf1fc8d01b99f18958cf4a6fbc5649fbd Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 29 Dec 2023 16:37:34 -0800 Subject: [PATCH 020/139] IO-2518 Dealership Vin Warning --- .../jobs-detail-header/jobs-detail-header.component.jsx | 5 +++++ 1 file changed, 5 insertions(+) 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 30bb98560..9ae08588d 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 @@ -221,6 +221,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {`${job.v_vin || t("general.labels.na")}`} + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + job.v_vin.length !== 17 ? ( + + ) : null + ) : null} {job.regie_number || t("general.labels.na")} From 9d3aca646b2effb73d3269b1b2f46c97af4e246f Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 3 Jan 2024 10:55:45 -0800 Subject: [PATCH 021/139] IO-2514 Production Board Estimator filter by table data --- .../production-list-columns.add.component.jsx | 7 ++-- .../production-list-columns.data.js | 16 ++++----- ...ction-list-table-view-select.component.jsx | 3 ++ .../production-list-table.component.jsx | 35 +++++++++++++++++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/client/src/components/production-list-columns/production-list-columns.add.component.jsx b/client/src/components/production-list-columns/production-list-columns.add.component.jsx index 82f884673..c79e9db7f 100644 --- a/client/src/components/production-list-columns/production-list-columns.add.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.add.component.jsx @@ -1,7 +1,7 @@ -import React from "react"; import { Button, Dropdown, Menu } from "antd"; -import dataSource from "./production-list-columns.data"; +import React from "react"; import { useTranslation } from "react-i18next"; +import dataSource from "./production-list-columns.data"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -24,6 +24,7 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop, + data, tableState, }) { const [columns, setColumns] = columnState; @@ -36,6 +37,7 @@ export function ProductionColumnsComponent({ bodyshop, technician, state: tableState, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).filter((i) => i.key === e.key), ]); @@ -46,6 +48,7 @@ export function ProductionColumnsComponent({ technician, state: tableState, activeStatuses: bodyshop.md_ro_statuses.active_statuses, + data: data, }); const menu = ( { +const r = ({ technician, state, activeStatuses, data, bodyshop }) => { return [ { title: i18n.t("jobs.actions.viewdetail"), @@ -548,14 +548,12 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { sortOrder: state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order, filters: - (bodyshop && - bodyshop.md_estimators.map((s) => { - return { - text: `${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim(), - value: [`${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim()], - }; - })) || - [], + data?.map((s) => { + return { + text: `${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim(), + value: [`${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim()], + }; + }) || [], onFilter: (value, record) => value.includes( `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() diff --git a/client/src/components/production-list-table/production-list-table-view-select.component.jsx b/client/src/components/production-list-table/production-list-table-view-select.component.jsx index 044c75994..82086ac96 100644 --- a/client/src/components/production-list-table/production-list-table-view-select.component.jsx +++ b/client/src/components/production-list-table/production-list-table-view-select.component.jsx @@ -24,6 +24,7 @@ export function ProductionListTable({ technician, currentUser, state, + data, setColumns, setState, }) { @@ -41,6 +42,7 @@ export function ProductionListTable({ bodyshop, technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width, @@ -95,6 +97,7 @@ export function ProductionListTable({ ...ProductionListColumns({ technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width, diff --git a/client/src/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx index 3ea3391d3..5257cea9d 100644 --- a/client/src/components/production-list-table/production-list-table.component.jsx +++ b/client/src/components/production-list-table/production-list-table.component.jsx @@ -10,7 +10,7 @@ import { Statistic, Table, } from "antd"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import ReactDragListView from "react-drag-listview"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -79,6 +79,7 @@ export function ProductionListTable({ bodyshop, technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width ?? 100, @@ -87,6 +88,33 @@ export function ProductionListTable({ [] ); + useEffect(() => { + const newColumns = + (state && + matchingColumnConfig && + matchingColumnConfig.columns.columnKeys.map((k) => { + return { + ...ProductionListColumns({ + bodyshop, + technician, + state, + data: data, + activeStatuses: bodyshop.md_ro_statuses.active_statuses, + }).find((e) => e.key === k.key), + width: k.width ?? 100, + }; + })) || + []; + setColumns(newColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + //state, + matchingColumnConfig, + bodyshop, + technician, + data, + ]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed. + const handleTableChange = (pagination, filters, sorter) => { setState({ ...state, @@ -104,7 +132,8 @@ export function ProductionListTable({ const removeColumn = (e) => { const { key } = e; - setColumns(columns.filter((i) => i.key !== key)); + const newColumns = columns.filter((i) => i.key !== key); + setColumns(newColumns); }; const handleResize = @@ -227,6 +256,7 @@ export function ProductionListTable({ Date: Fri, 5 Jan 2024 08:53:17 -0800 Subject: [PATCH 022/139] Minor intellipay change. --- server/intellipay/intellipay.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 0ece0c69c..60b74c1b5 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -132,6 +132,7 @@ exports.payment_refund = async (req, res) => { exports.generate_payment_url = async (req, res) => { logger.log("intellipay-payment-url", "DEBUG", req.user?.email, null, null); const shopCredentials = await getShopCredentials(req.body.bodyshop); + try { const options = { method: "POST", @@ -139,7 +140,12 @@ exports.generate_payment_url = async (req, res) => { //TODO: Move these to environment variables/database. data: qs.stringify({ ...shopCredentials, - ...req.body, + //...req.body, + amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat( + "0.00" + ), + account: req.body.account, + invoice: req.body.invoice, createshorturl: true, //The postback URL is set at the CP teller global terminal settings page. }), From c0dab92d0ef6d0f2f7b51129d556edd2aae9d2c1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 5 Jan 2024 12:01:47 -0800 Subject: [PATCH 023/139] IO-2522 Load Level Table Change --- hasura/metadata/tables.yaml | 33 ++++++++++--------- .../down.sql | 4 +++ .../up.sql | 2 ++ .../down.sql | 3 ++ .../up.sql | 1 + .../down.sql | 4 +++ .../up.sql | 2 ++ 7 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql create mode 100644 hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql create mode 100644 hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql create mode 100644 hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql create mode 100644 hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql create mode 100644 hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 216ac8c48..9867b3f8e 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2023,24 +2023,24 @@ - active: _eq: true columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at select_permissions: - role: user permission: columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at filter: employee_team: bodyshop: @@ -2055,13 +2055,13 @@ - role: user permission: columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at filter: employee_team: bodyshop: @@ -2123,21 +2123,23 @@ _eq: true columns: - active - - name - - created_at - - updated_at - bodyshopid + - created_at - id + - max_load + - name + - updated_at select_permissions: - role: user permission: columns: - active - - name - - created_at - - updated_at - bodyshopid + - created_at - id + - max_load + - name + - updated_at filter: bodyshop: associations: @@ -2153,6 +2155,7 @@ columns: - active - bodyshopid + - max_load - name - updated_at filter: diff --git a/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql new file mode 100644 index 000000000..8df675f00 --- /dev/null +++ b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."employee_team_members" add column "max_load" numeric +-- not null default '10000'; diff --git a/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql new file mode 100644 index 000000000..7b49a8dcd --- /dev/null +++ b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql @@ -0,0 +1,2 @@ +alter table "public"."employee_team_members" add column "max_load" numeric + not null default '10000'; diff --git a/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql new file mode 100644 index 000000000..a869e6890 --- /dev/null +++ b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql @@ -0,0 +1,3 @@ +alter table "public"."employee_team_members" alter column "max_load" set default '10000'::numeric; +alter table "public"."employee_team_members" alter column "max_load" drop not null; +alter table "public"."employee_team_members" add column "max_load" numeric; diff --git a/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql new file mode 100644 index 000000000..18bf59cf3 --- /dev/null +++ b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql @@ -0,0 +1 @@ +alter table "public"."employee_team_members" drop column "max_load" cascade; diff --git a/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql new file mode 100644 index 000000000..58a0f9b6d --- /dev/null +++ b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."employee_teams" add column "max_load" numeric +-- not null default '10000'; diff --git a/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql new file mode 100644 index 000000000..c55ba33d0 --- /dev/null +++ b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql @@ -0,0 +1,2 @@ +alter table "public"."employee_teams" add column "max_load" numeric + not null default '10000'; From fe3698980d5c81651288ed3aad1807366e5e7fa2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 5 Jan 2024 13:09:15 -0800 Subject: [PATCH 024/139] IO-2514 Only Unique items in Menu --- .../production-list-columns.data.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 d43433540..8e584aa2e 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 @@ -6,6 +6,7 @@ import { Link } from "react-router-dom"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { TimeFormatter } from "../../utils/DateFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import JobAltTransportChange from "../job-at-change/job-at-change.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; @@ -548,12 +549,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => { sortOrder: state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order, filters: - data?.map((s) => { - return { - text: `${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim(), - value: [`${s.est_ct_fn || ""} ${s.est_ct_ln || ""}`.trim()], - }; - }) || [], + (data && + data + .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() From 25429e78f8d711b1d2efb4430f4d1371ccc4d57f Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 8 Jan 2024 10:37:19 -0800 Subject: [PATCH 025/139] IO-2518 Null coalesce for v_vin for warning --- .../jobs-detail-header/jobs-detail-header.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ae08588d..958f7f1d5 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 @@ -222,7 +222,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {`${job.v_vin || t("general.labels.na")}`} {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( - job.v_vin.length !== 17 ? ( + job.v_vin?.length !== 17 ? ( ) : null ) : null} From 1305277c092c3d589f18933fb1750cf6c05f671a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 9 Jan 2024 11:08:02 -0800 Subject: [PATCH 026/139] IO-2520 Kaizen Data Pump --- server.js | 1 + server/data/claimscorp.js | 2 +- server/data/data.js | 3 +- server/data/kaizen.js | 830 +++++++++++++++++++++++++++++++ server/graphql-client/queries.js | 199 +++++++- 5 files changed, 1032 insertions(+), 3 deletions(-) create mode 100644 server/data/kaizen.js diff --git a/server.js b/server.js index f16d583d5..a64545b72 100644 --- a/server.js +++ b/server.js @@ -224,6 +224,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); app.post("/data/cc", data.claimscorp); +app.post("/data/kaizen", data.kaizen); app.post("/record-handler/arms", data.arms); var taskHandler = require("./server/tasks/tasks"); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 60943aed3..1768f6eb1 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -507,7 +507,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat), Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), - Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), + Frame: repairCosts.FrameLaborTotalCost.toFormat(CCDineroFormat), Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat), Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat), Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat), diff --git a/server/data/data.js b/server/data/data.js index 077f9f134..18ec4c321 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,3 +1,4 @@ +exports.arms = require("./arms").default; exports.autohouse = require("./autohouse").default; exports.claimscorp = require("./claimscorp").default; -exports.arms = require("./arms").default; \ No newline at end of file +exports.kaizen = require("./kaizen").default; \ No newline at end of file diff --git a/server/data/kaizen.js b/server/data/kaizen.js new file mode 100644 index 000000000..57c354e4e --- /dev/null +++ b/server/data/kaizen.js @@ -0,0 +1,830 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const Dinero = require("dinero.js"); +const moment = require("moment-timezone"); +var builder = require("xmlbuilder2"); +const _ = require("lodash"); +const logger = require("../utils/logger"); +const fs = require("fs"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +let Client = require("ssh2-sftp-client"); + +const client = require("../graphql-client/graphql-client").client; +const { sendServerEmail } = require("../email/sendemail"); +const DineroFormat = "0,0.00"; +const DateFormat = "MM/DD/YYYY"; + +const repairOpCodes = ["OP4", "OP9", "OP10"]; +const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; + +const ftpSetup = { + host: process.env.KAIZEN_HOST, + port: process.env.KAIZEN_PORT, + username: process.env.KAIZEN_USER, + password: process.env.KAIZEN_PASSWORD, + debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), + algorithms: { + serverHostKey: ["ssh-rsa", "ssh-dss"], + }, +}; + +exports.default = async (req, res) => { + //Query for the List of Bodyshop Clients. + logger.log("kaizen-start", "DEBUG", "api", null, null); + const kaizenShopsNames = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; + + const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { + shopname: kaizenShopsNames, + }); + + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + res.sendStatus(401); + return; + } + const allxmlsToUpload = []; + const allErrors = []; + try { + for (const bodyshop of specificShopIds + ? bodyshops.filter((b) => specificShopIds.includes(b.id)) + : bodyshops) { + logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + const erroredJobs = []; + try { + const { jobs, bodyshops_by_pk } = await client.request( + queries.KAIZEN_QUERY, + { + bodyshopid: bodyshop.id, + start: start + ? moment(start).startOf("day") + : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }), + } + ); + + const kaizenObject = { + DataFeed: { + ShopInfo: { + ShopName: bodyshops_by_pk.shopname, + Jobs: jobs.map((j) => + CreateRepairOrderTag( + { ...j, bodyshop: bodyshops_by_pk }, + function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + } + ) + ), + }, + }, + }; + + if (erroredJobs.length > 0) { + logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)), + }); + } + + var ret = builder + .create( + { + // version: "1.0", + // encoding: "UTF-8", + //keepNullNodes: true, + }, + kaizenObject + ) + .end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: kaizenObject.DataFeed.ShopInfo.Jobs.length, + xml: ret, + filename: `${bodyshop.shopname}-${moment().format( + "YYYYMMDDTHHMMss" + )}.xml`, + }); + + logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + } catch (error) { + //Error at the shop level. + logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { + ...error, + }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()], + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error, + })), + }); + } + } + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + + res.json(allxmlsToUpload); + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + return; + } + + let sftp = new Client(); + sftp.on("error", (errors) => + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...errors, + }) + ); + try { + //Connect to the FTP and upload all. + + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { + filename: xmlObj.filename, + }); + + const uploadResult = await sftp.put( + Buffer.from(xmlObj.xml), + `/${xmlObj.filename}` + ); + logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { + uploadResult, + }); + } + + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + } catch (error) { + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...error, + }); + } finally { + sftp.end(); + } + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + res.sendStatus(200); + } catch (error) { + res.status(200).json(error); + } +}; + +const CreateRepairOrderTag = (job, errorCallback) => { + //Level 2 + + if (!job.job_totals) { + errorCallback({ + jobid: job.id, + job: job, + ro_number: job.ro_number, + error: { toString: () => "No job totals for RO." }, + }); + return {}; + } + + const repairCosts = CreateCosts(job); + + try { + const ret = { + JobID: job.id, + RoNumber: job.ro_number, + JobStatus: job.tlos_ind + ? "Total Loss" + : job.ro_number + ? job.status + : "Estimate", + Customer: { + CompanyName: job.ownr_co_nm?.trim() || "", + FirstName: job.ownr_fn?.trim() || "", + LastName: job.ownr_ln?.trim() || "", + Address1: job.ownr_addr1?.trim() || "", + Address2: job.ownr_addr2?.trim() || "", + City: job.ownr_city?.trim() || "", + State: job.ownr_st?.trim() || "", + Zip: job.ownr_zip?.trim() || "", + }, + Vehicle: { + Year: job.v_model_yr + ? parseInt(job.v_model_yr.match(/\d/g)) + ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) + : "" + : "", + Make: job.v_make_desc || "", + Model: job.v_model_desc || "", + BodyStyle: job.vehicle?.v_bstyle || "", + Color: job.v_color || "", + VIN: job.v_vin || "", + PlateNo: job.plate_no || "", + }, + InsuranceCompany: job.ins_co_nm || "", + Claim: job.clm_no || "", + Contacts: { + CSR: job.employee_csr_rel + ? `${ + job.employee_csr_rel.last_name + ? job.employee_csr_rel.last_name + : "" + }${job.employee_csr_rel.last_name ? ", " : ""}${ + job.employee_csr_rel.first_name + ? job.employee_csr_rel.first_name + : "" + }` + : "", + Estimator: `${job.est_ct_ln ? job.est_ct_ln : ""}${ + job.est_ct_ln ? ", " : "" + }${job.est_ct_fn ? job.est_ct_fn : ""}`, + }, + Dates: { + DateEstimated: + (job.date_estimated && + moment(job.date_estimated).format(DateFormat)) || + "", + DateOpened: + (job.date_opened && moment(job.date_opened).format(DateFormat)) || "", + DateScheduled: + (job.scheduled_in && + moment(job.scheduled_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateArrived: + (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateStart: job.date_repairstarted + ? (job.date_repairstarted && + moment(job.date_repairstarted) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "" + : (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledCompletion: + (job.scheduled_completion && + moment(job.scheduled_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateCompleted: + (job.actual_completion && + moment(job.actual_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledDelivery: + (job.scheduled_delivery && + moment(job.scheduled_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateDelivered: + (job.actual_delivery && + moment(job.actual_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateInvoiced: + (job.date_invoiced && + moment(job.date_invoiced) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateExported: + (job.date_exported && + moment(job.date_exported) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + }, + Sales: { + Labour: { + Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat( + DineroFormat + ), + Body: Dinero(job.job_totals.rates.lab.total).toFormat(DineroFormat), + Diagnostic: Dinero(job.job_totals.rates.lad.total).toFormat( + DineroFormat + ), + Electrical: Dinero(job.job_totals.rates.lae.total).toFormat( + DineroFormat + ), + Frame: Dinero(job.job_totals.rates.laf.total).toFormat(DineroFormat), + Glass: Dinero(job.job_totals.rates.lag.total).toFormat(DineroFormat), + Mechanical: Dinero(job.job_totals.rates.lam.total).toFormat( + DineroFormat + ), + OtherLabour: Dinero(job.job_totals.rates.la1.total) + .add(Dinero(job.job_totals.rates.la2.total)) + .add(Dinero(job.job_totals.rates.la3.total)) + .add(Dinero(job.job_totals.rates.la4.total)) + .add(Dinero(job.job_totals.rates.lau.total)) + .toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.lar.total).toFormat( + DineroFormat + ), + Structural: Dinero(job.job_totals.rates.las.total).toFormat( + DineroFormat + ), + }, + Materials: { + Body: Dinero(job.job_totals.rates.mash.total).toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.mapa.total).toFormat( + DineroFormat + ), + }, + Parts: { + Aftermarket: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ).toFormat(DineroFormat), + LKQ: Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ).toFormat(DineroFormat), + OEM: Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAP && + job.job_totals.parts.parts.list.PAP.total + ) + ) + .toFormat(DineroFormat), + OtherParts: Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ).toFormat(DineroFormat), + Reconditioned: Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ).toFormat(DineroFormat), + TotalParts: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ) + ) + .toFormat(DineroFormat), + }, + OtherSales: Dinero(job.job_totals.additional.storage).toFormat( + DineroFormat + ), + Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat( + DineroFormat + ), + Towing: Dinero(job.job_totals.additional.towing).toFormat(DineroFormat), + ATS: + job.job_totals.additional.additionalCostItems.includes( + "ATS Amount" + ) === true + ? Dinero( + job.job_totals.additional.additionalCostItems[ + job.job_totals.additional.additionalCostItems.indexOf( + "ATS Amount" + ) + ].total + ).toFormat(DineroFormat) + : Dinero().toFormat(DineroFormat), + SaleSubtotal: Dinero(job.job_totals.totals.subtotal).toFormat( + DineroFormat + ), + Tax: Dinero(job.job_totals.totals.local_tax) + .add(Dinero(job.job_totals.totals.state_tax)) + .add(Dinero(job.job_totals.totals.federal_tax)) + .add(Dinero(job.job_totals.additional.pvrt)) + .toFormat(DineroFormat), + SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat( + DineroFormat + ), + }, + SaleHours: { + Aluminum: job.job_totals.rates.laa.hours.toFixed(2), + Body: job.job_totals.rates.lab.hours.toFixed(2), + Diagnostic: job.job_totals.rates.lad.hours.toFixed(2), + Electrical: job.job_totals.rates.lae.hours.toFixed(2), + Frame: job.job_totals.rates.laf.hours.toFixed(2), + Glass: job.job_totals.rates.lag.hours.toFixed(2), + Mechanical: job.job_totals.rates.lam.hours.toFixed(2), + Other: ( + job.job_totals.rates.la1.hours + + job.job_totals.rates.la2.hours + + job.job_totals.rates.la3.hours + + job.job_totals.rates.la4.hours + + job.job_totals.rates.lau.hours + ).toFixed(2), + Refinish: job.job_totals.rates.lar.hours.toFixed(2), + Structural: job.job_totals.rates.las.hours.toFixed(2), + TotalHours: job.joblines + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + .toFixed(2), + }, + Costs: { + Labour: { + Aluminum: repairCosts.AluminumLabourTotalCost.toFormat(DineroFormat), + Body: repairCosts.BodyLabourTotalCost.toFormat(DineroFormat), + Diagnostic: + repairCosts.DiagnosticLabourTotalCost.toFormat(DineroFormat), + Electrical: + repairCosts.ElectricalLabourTotalCost.toFormat(DineroFormat), + Frame: repairCosts.FrameLabourTotalCost.toFormat(DineroFormat), + Glass: repairCosts.GlassLabourTotalCost.toFormat(DineroFormat), + Mechancial: + repairCosts.MechanicalLabourTotalCost.toFormat(DineroFormat), + OtherLabour: repairCosts.LabourMiscTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.RefinishLabourTotalCost.toFormat(DineroFormat), + Structural: + repairCosts.StructuralLabourTotalCost.toFormat(DineroFormat), + TotalLabour: repairCosts.LabourTotalCost.toFormat(DineroFormat), + }, + Materials: { + Body: repairCosts.BMTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.PMTotalCost.toFormat(DineroFormat), + }, + Parts: { + Aftermarket: repairCosts.PartsAMCost.toFormat(DineroFormat), + LKQ: repairCosts.PartsRecycledCost.toFormat(DineroFormat), + OEM: repairCosts.PartsOemCost.toFormat(DineroFormat), + OtherCost: repairCosts.PartsOtherCost.toFormat(DineroFormat), + Reconditioned: + repairCosts.PartsReconditionedCost.toFormat(DineroFormat), + TotalParts: repairCosts.PartsAMCost.add(repairCosts.PartsRecycledCost) + .add(repairCosts.PartsReconditionedCost) + .add(repairCosts.PartsOemCost) + .add(repairCosts.PartsOtherCost) + .toFormat(DineroFormat), + }, + Sublet: repairCosts.SubletTotalCost.toFormat(DineroFormat), + Towing: repairCosts.TowingTotalCost.toFormat(DineroFormat), + ATS: Dinero().toFormat(DineroFormat), + Storage: repairCosts.StorageTotalCost.toFormat(DineroFormat), + CostTotal: repairCosts.TotalCost.toFormat(DineroFormat), + }, + CostHours: { + Aluminum: repairCosts.AluminumLabourTotalHrs.toFixed(2), + Body: repairCosts.BodyLabourTotalHrs.toFixed(2), + Diagnostic: repairCosts.DiagnosticLabourTotalHrs.toFixed(2), + Refinish: repairCosts.RefinishLabourTotalHrs.toFixed(2), + Frame: repairCosts.FrameLabourTotalHrs.toFixed(2), + Mechanical: repairCosts.MechanicalLabourTotalHrs.toFixed(2), + Glass: repairCosts.GlassLabourTotalHrs.toFixed(2), + Electrical: repairCosts.ElectricalLabourTotalHrs.toFixed(2), + Structural: repairCosts.StructuralLabourTotalHrs.toFixed(2), + Other: repairCosts.LabourMiscTotalHrs.toFixed(2), + CostTotalHours: repairCosts.TotalHrs.toFixed(2), + }, + }; + return ret; + } catch (error) { + logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { + error, + }); + + errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); + } +}; + +const CreateCosts = (job) => { + //Create a mapping based on AH Requirements + + //For DMS, the keys in the object below are the CIECA part types. + const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + + if (!bill_acc[line_val.cost_center]) + bill_acc[line_val.cost_center] = Dinero(); + + bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + + return null; + }); + return bill_acc; + }, {}); + + //If the hourly rates for job costing are set, add them in. + if ( + job.bodyshop.jc_hourly_rates && + (job.bodyshop.jc_hourly_rates.mapa || + typeof job.bodyshop.jc_hourly_rates.mapa === "number" || + isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) + ) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero({ + amount: Math.round( + ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 + ), + }); + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = Dinero(); + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mash * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mash.hours) + ); + } + //Uses CIECA Labour types. + const ticketTotalsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = Dinero(); + + ticket_acc[ticket_val.cost_center] = ticket_acc[ + ticket_val.cost_center + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ) + ); + + return ticket_acc; + }, + {} + ); + const ticketHrsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = 0; + + ticket_acc[ticket_val.cost_center] = + ticket_acc[ticket_val.cost_center] + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0; + + return ticket_acc; + }, + {} + ); + //CIECA STANDARD MAPPING OBJECT. + + const ciecaObj = { + ATS: "ATS", + LA1: "LA1", + LA2: "LA2", + LA3: "LA3", + LA4: "LA4", + LAA: "LAA", + LAB: "LAB", + LAD: "LAD", + LAE: "LAE", + LAF: "LAF", + LAG: "LAG", + LAM: "LAM", + LAR: "LAR", + LAS: "LAS", + LAU: "LAU", + PAA: "PAA", + PAC: "PAC", + PAG: "PAG", + PAL: "PAL", + PAM: "PAM", + PAN: "PAN", + PAO: "PAO", + PAP: "PAP", + PAR: "PAR", + PAS: "PAS", + TOW: "TOW", + MAPA: "MAPA", + MASH: "MASH", + PASL: "PASL", + }; + const defaultCosts = + job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber + ? ciecaObj + : job.bodyshop.md_responsibility_centers.defaults.costs; + + return { + PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + if ( + key !== defaultCosts.PAS && + key !== defaultCosts.PASL && + key !== defaultCosts.MAPA && + key !== defaultCosts.MASH && + key !== defaultCosts.TOW + ) + return acc.add(billTotalsByCostCenters[key]); + return acc; + }, Dinero()), + PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( + billTotalsByCostCenters[defaultCosts.PAP] || Dinero() + ), + PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), + PartsReconditionedCost: + billTotalsByCostCenters[defaultCosts.PAM] || Dinero(), + PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(), + PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + + SubletTotalCost: + billTotalsByCostCenters[defaultCosts.PAS] || + Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()), + + AluminumLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAA] || Dinero(), + AluminumLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAA] || 0, + BodyLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), + BodyLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0, + DiagnosticLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(), + DiagnosticLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAD] || 0, + ElectricalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(), + ElectricalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0, + FrameLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(), + FrameLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0, + GlassLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(), + GlassLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0, + LabourMiscTotalCost: ( + ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero() + ) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()), + LabourMiscTotalHrs: + (ticketHrsByCostCenter[defaultCosts.LA1] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA2] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA3] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA4] || 0) + + (ticketHrsByCostCenter[defaultCosts.LAU] || 0), + MechanicalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), + MechanicalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0, + RefinishLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), + RefinishLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0, + StructuralLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), + StructuralLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0, + + PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(), + BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(), + + MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(), + StorageTotalCost: Dinero(), + DetailTotal: Dinero(), + DetailTotalCost: Dinero(), + + SalesTaxTotalCost: Dinero(), + LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce( + (acc, key) => { + return acc.add(ticketTotalsByCostCenter[key]); + }, + Dinero() + ), + TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + return acc.add(billTotalsByCostCenters[key]); + }, Dinero()), + TotalHrs: job.timetickets.reduce((acc, ticket_val) => { + return ( + acc + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ); + }, 0), + }; +}; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a27ac8bd0..554388afe 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1070,6 +1070,183 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) { } }`; +exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + address1 + city + state + zip_post + country + phone + last_name_first + md_ro_statuses + md_order_statuses + md_responsibility_centers + jc_hourly_rates + cdk_dealerid + pbs_serialnumber + use_paint_scale_data + timezone + } + jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { + actual_completion + actual_delivery + actual_in + asgn_date + bills { + billlines { + actual_cost + cost_center + id + quantity + } + federal_tax_rate + id + is_credit_memo + local_tax_rate + state_tax_rate + } + created_at + clm_no + date_estimated + date_exported + date_invoiced + date_open + date_repairstarted + employee_body_rel { + first_name + last_name + employee_number + id + } + employee_csr_rel { + first_name + last_name + employee_number + id + } + employee_prep_rel { + first_name + last_name + employee_number + id + } + employee_refinish_rel { + first_name + last_name + employee_number + id + } + est_ct_fn + est_ct_ln + id + ins_co_nm + joblines(where: {removed: {_eq: false}}) { + act_price + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { + actual_cost + actual_price + quantity + bill { + vendor { + name + } + invoice_number + date + } + } + db_price + id + lbr_op + line_desc + line_ind + line_no + mod_lb_hrs + mod_lbr_ty + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + part_qty + part_type + profitcenter_part + profitcenter_labor + prt_dsmk_m + prt_dsmk_p + oem_partno + status + } + job_totals + loss_date + mixdata(limit: 1, order_by: {updated_at: desc}) { + jobid + totalliquidcost + } + ownr_addr1 + ownr_addr2 + ownr_city + ownr_co_nm + ownr_fn + ownr_ln + ownr_st + ownr_zip + parts_orders(limit: 1, order_by: {created_at: desc}) { + created_at + } + parts_tax_rates + plate_no + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_matd + rate_mapa + rate_mash + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + status + timetickets { + id + rate + cost_center + actualhrs + productivehrs + flat_rate + } + tlos_ind + v_color + v_model_yr + v_model_desc + v_make_desc + v_vin + vehicle { + v_bstyle + } + } +}`; + exports.UPDATE_JOB = ` mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) { update_jobs(where: { id: { _eq: $jobId } }, _set: $job) { @@ -1542,7 +1719,7 @@ exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { } }`; -exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { +exports.GET_ENTEGRAL_SHOPS = `query GET_ENTEGRAL_SHOPS { bodyshops(where: {entegral_id: {_is_null: false}, _or: {entegral_id: {_neq: ""}}}){ id shopname @@ -1562,6 +1739,26 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { } }`; +exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($shopname: [String]) { + bodyshops(where: {shopname: {_in: $shopname}}){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + autohouseid + md_responsibility_centers + jc_hourly_rates + imexshopid + timezone + } +}`; + exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{ delete_dms_vehicles(where: {}) { affected_rows From 3e9279d89a9d27ec5ab4b6935937bdbbfbfef2f9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 9 Jan 2024 12:00:24 -0800 Subject: [PATCH 027/139] IO-2520 Change Query Time Bound --- server/data/kaizen.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 57c354e4e..7cd7fef8c 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -64,9 +64,9 @@ exports.default = async (req, res) => { { bodyshopid: bodyshop.id, start: start - ? moment(start).startOf("day") - : moment().subtract(5, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }), + ? moment(start).startOf("hours") + : moment().subtract(2, "hours").startOf("hour"), + ...(end && { end: moment(end).endOf("hours") }), } ); From 02b6875eecd5ed98e61a88228cfea9a37bcd942e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 15:41:10 -0800 Subject: [PATCH 028/139] 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 04cff4acb15d806877592077a96dc9cd27e053a1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 16:26:09 -0800 Subject: [PATCH 029/139] IO-2520 Add in Server Key format --- server/data/kaizen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 7cd7fef8c..0df82a9ce 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -29,7 +29,7 @@ const ftpSetup = { password: process.env.KAIZEN_PASSWORD, debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), algorithms: { - serverHostKey: ["ssh-rsa", "ssh-dss"], + serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"], }, }; From 4c4e16b0c902cf0ba55f9bfd060c0cd48ffc35d0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 12 Jan 2024 20:47:06 -0500 Subject: [PATCH 030/139] - 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 031/139] - 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 ebe5c5b11397485e9a45f0e184b6aabffb3caac8 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 21:06:39 -0800 Subject: [PATCH 032/139] IO-2520 Adjust to imexshopid instead of shopname & prettify --- server/data/kaizen.js | 13 ++++++++++--- server/graphql-client/queries.js | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 0df82a9ce..0d3720d25 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -29,17 +29,24 @@ const ftpSetup = { password: process.env.KAIZEN_PASSWORD, debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), algorithms: { - serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"], + serverHostKey: [ + "ssh-rsa", + "ssh-dss", + "rsa-sha2-256", + "rsa-sha2-512", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + ], }, }; exports.default = async (req, res) => { //Query for the List of Bodyshop Clients. logger.log("kaizen-start", "DEBUG", "api", null, null); - const kaizenShopsNames = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; + const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { - shopname: kaizenShopsNames, + imexshopid: kaizenShopsIDs, }); const specificShopIds = req.body.bodyshopIds; // ['uuid] diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 554388afe..b9c491de7 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1739,8 +1739,8 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_ENTEGRAL_SHOPS { } }`; -exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($shopname: [String]) { - bodyshops(where: {shopname: {_in: $shopname}}){ +exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($imexshopid: [String]) { + bodyshops(where: {imexshopid: {_in: $imexshopid}}){ id shopname address1 From 7245d4eab24cdb682c4fa9bca639eabff9e8be02 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 15 Jan 2024 18:54:28 -0800 Subject: [PATCH 033/139] 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 034/139] 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 035/139] 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 036/139] 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 037/139] 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 038/139] 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 039/139] 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 040/139] - 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 041/139] 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 042/139] - 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 043/139] 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 044/139] - 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 045/139] - 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 046/139] - 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 047/139] 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 048/139] 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 064/139] - 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 065/139] - 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 066/139] - 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 067/139] - 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 068/139] - 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 069/139] - 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 070/139] - 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 071/139] 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 072/139] 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 073/139] 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 074/139] - 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 075/139] - 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 076/139] 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 077/139] - 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 078/139] 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 079/139] - 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 080/139] - 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 081/139] 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 088/139] 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 089/139] 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 090/139] 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 091/139] 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 092/139] 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 093/139] 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 094/139] 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 095/139] 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 096/139] 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 097/139] 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 098/139] 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 099/139] 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 100/139] 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 101/139] 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 102/139] 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 103/139] 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 104/139] 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 105/139] 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 106/139] 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 107/139] 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 108/139] 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 109/139] 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 110/139] - 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 111/139] 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 112/139] - 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 113/139] - 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 114/139] - 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 115/139] - 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 116/139] - 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 120/139] - 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 121/139] 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 122/139] 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 123/139] 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 124/139] 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 125/139] 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 126/139] - 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 127/139] 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 128/139] 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 129/139] - 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 137/139] - 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 138/139] 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 139/139] 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 (