IO-2626 CSI Pages

Move to Server side initial commit
This commit is contained in:
Allan Carr
2024-02-02 11:55:57 -08:00
parent c8fc1b0f68
commit 830d2c87d2
11 changed files with 293 additions and 147 deletions

View File

@@ -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 ? (

View File

@@ -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 (
<div>
The Config Form {readOnly}
{readOnly}
{selectedCsi && (
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents

View File

@@ -1,7 +1,7 @@
import { CheckCircleFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Col, List, Row } from "antd";
import React, { useState } from "react";
import { useQuery } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
@@ -21,7 +21,6 @@ export default function ShopCsiConfig() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
The Config Form
<Row>
<Col span={3}>
<List
@@ -42,7 +41,8 @@ export default function ShopCsiConfig() {
)}
/>
</Col>
<Col span={21}>
<Col span={1}/>
<Col span={20}>
<ShopCsiConfigForm selectedCsi={selectedCsi} />
</Col>
</Row>

View File

@@ -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 <LoadingSpinner />;
if (error || !!!data.csi_by_pk)
return (
<div>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
>
{error ? (
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
) : null}
</Result>
</div>
);
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 (
<Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
@@ -94,85 +68,170 @@ export function CsiContainerPage({ currentUser }) {
/>
</Layout>
);
}
return (
<Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img src={bodyshop.logo_img_path.src} alt="Logo" />
) : null}
<div style={{ margin: "2em" }}>
<strong>{bodyshop.shopname || ""}</strong>
<div>{`${bodyshop.address1 || ""}`}</div>
<div>{`${bodyshop.address2 || ""}`}</div>
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
bodyshop.zip_post || ""
}`}</div>
if (submitting.loading) return <LoadingSpinner />;
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 (
<>
<Layout style={{ display: "flex", flexDirection: "column" }}>
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Form>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
/>
</Form>
</Layout.Content>
<Layout.Footer>
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
</Layout.Footer>
</Layout>
</>
);
} else {
const {
relateddata: { bodyshop, job },
csiquestion: { config: csiquestions },
} = axiosResponse.csi_by_pk;
return (
<Layout style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img src={bodyshop.logo_img_path.src} alt="Logo" />
) : null}
<div style={{ margin: "2em", verticalAlign: "middle" }}>
<Typography.Title level={4} style={{ margin: 0 }}>
{bodyshop.shopname || ""}
</Typography.Title>
<Typography.Paragraph style={{ margin: 0 }}>
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
bodyshop.address2 || ""
}`.trim()}
</Typography.Paragraph>
<Typography.Paragraph style={{ margin: 0 }}>
{`${bodyshop.city || ""}${
bodyshop.city && bodyshop.state ? ", " : ""
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
</Typography.Paragraph>
</div>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>
{t("csi.labels.greeting", {
name: job.ownr_co_nm || job.ownr_fn || "",
})}
</strong>
<Typography.Paragraph>
{t("csi.labels.intro", { shopname: bodyshop.shopname || "" })}
</Typography.Paragraph>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
<Typography.Paragraph>
{`At ${
bodyshop.shopname || ""
}, we value your feedback. We would love to
hear what you have to say. Please fill out the form below.`}
</Typography.Paragraph>
</div>
{submitting.error ? (
<AlertComponent message={submitting.error} type="error" />
) : null}
{submitting.error ? (
<AlertComponent message={submitting.error} type="error" />
) : null}
{submitting.submitted ? (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Result
status="success"
title={t("csi.successes.submitted")}
subTitle={t("csi.successes.submittedsub")}
/>
</Layout.Content>
) : (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents componentList={csiquestions} />
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
>
{t("general.actions.submit")}
</Button>
</Form>
</Layout.Content>
)}
<Layout.Footer>
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
</Layout.Footer>
</Layout>
);
{submitting.submitted ? (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Result
status="success"
title={t("csi.successes.submitted")}
subTitle={t("csi.successes.submittedsub")}
/>
</Layout.Content>
) : (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Form form={form} onFinish={handleFinish}>
{axiosResponse.csi_by_pk.valid ? (
<>
<ConfigFormComponents componentList={csiquestions} />
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
style={{ float: "right" }}
>
{t("general.actions.submit")}
</Button>
</>
) : (
<>
<Result
title={t("csi.errors.surveycompletetitle")}
status="warning"
subTitle={t("csi.errors.surveycompletesubtitle", {
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
})}
/>
<Typography.Paragraph
type="secondary"
style={{ textAlign: "center" }}
>
{t("csi.successes.submittedsub")}
</Typography.Paragraph>
</>
)}
</Form>
</Layout.Content>
)}
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", { surveyId: surveyId })}
</Layout.Footer>
</Layout>
);
}
}

View File

@@ -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. ",

View File

@@ -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) => {

2
server/csi/csi.js Normal file
View File

@@ -0,0 +1,2 @@
exports.lookup = require("./lookup").default;
exports.submit = require("./submit").default;

24
server/csi/lookup.js Normal file
View File

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

29
server/csi/submit.js Normal file
View File

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

View File

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

View File

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