From a74a9ba5a16198e322e506b977ff1d62e6a6e1c2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 31 Jan 2024 09:59:56 -0800 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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" ] }