From 54859ced34b7622f25ad74a5e829cd6405269368 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 16:51:19 -0500 Subject: [PATCH 01/38] Add env variable for 2023 change request form path --- app/server/.env.example | 1 + app/server/app/config/formio.js | 7 +++++-- app/server/app/index.js | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/server/.env.example b/app/server/.env.example index 334f7a6d..ead1c86e 100644 --- a/app/server/.env.example +++ b/app/server/.env.example @@ -22,6 +22,7 @@ FORMIO_2022_CRF_PATH= FORMIO_2023_FRF_PATH= FORMIO_2023_PRF_PATH= FORMIO_2023_CRF_PATH= +FORMIO_2023_CHANGE_PATH= FORMIO_BASE_URL= FORMIO_PROJECT_NAME= FORMIO_API_KEY= diff --git a/app/server/app/config/formio.js b/app/server/app/config/formio.js index 68f90b47..e3ed17ac 100644 --- a/app/server/app/config/formio.js +++ b/app/server/app/config/formio.js @@ -21,6 +21,7 @@ const { FORMIO_2023_FRF_PATH, FORMIO_2023_PRF_PATH, FORMIO_2023_CRF_PATH, + FORMIO_2023_CHANGE_PATH, } = process.env; const formioProjectUrl = `${FORMIO_BASE_URL}/${FORMIO_PROJECT_NAME}`; @@ -33,11 +34,13 @@ const formUrl = { frf: `${formioProjectUrl}/${FORMIO_2022_FRF_PATH}`, prf: `${formioProjectUrl}/${FORMIO_2022_PRF_PATH}`, crf: `${formioProjectUrl}/${FORMIO_2022_CRF_PATH}`, + change: "", // NOTE: change request form was added in the 2023 rebate year }, 2023: { frf: `${formioProjectUrl}/${FORMIO_2023_FRF_PATH}`, prf: `${formioProjectUrl}/${FORMIO_2023_PRF_PATH}`, crf: `${formioProjectUrl}/${FORMIO_2023_CRF_PATH}`, + change: `${formioProjectUrl}/${FORMIO_2023_CHANGE_PATH}`, }, }; @@ -97,7 +100,7 @@ function axiosFormio(req) { log({ level: "warn", message: logMessage, req: config }); return new Promise((resolve) => - setTimeout(() => resolve(instance.request(config)), 1000) + setTimeout(() => resolve(instance.request(config)), 1000), ); } @@ -107,7 +110,7 @@ function axiosFormio(req) { log({ level: "error", message: logMessage, req: config }); return Promise.reject(error); - } + }, ); return instance; diff --git a/app/server/app/index.js b/app/server/app/index.js index 8e0176b5..971388d5 100644 --- a/app/server/app/index.js +++ b/app/server/app/index.js @@ -44,6 +44,7 @@ const requiredEnvironmentVariables = [ "FORMIO_2023_FRF_PATH", "FORMIO_2023_PRF_PATH", "FORMIO_2023_CRF_PATH", + "FORMIO_2023_CHANGE_PATH", "FORMIO_BASE_URL", "FORMIO_PROJECT_NAME", "FORMIO_API_KEY", From 9ba4c78d5f8127d5502e6a07c55e1f5de1e644fe Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 17:48:01 -0500 Subject: [PATCH 02/38] Add server routes and formio requests for 2023 change request form --- app/server/app/routes/formio2023.js | 21 +++++-- app/server/app/utilities/formio.js | 89 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/app/server/app/routes/formio2023.js b/app/server/app/routes/formio2023.js index a8e46dca..7bb5c595 100644 --- a/app/server/app/routes/formio2023.js +++ b/app/server/app/routes/formio2023.js @@ -19,6 +19,9 @@ const { fetchPRFSubmission, updatePRFSubmission, deletePRFSubmission, + // + fetchChangeSubmissions, + createChangeSubmission, } = require("../utilities/formio"); const rebateYear = "2023"; @@ -99,16 +102,26 @@ router.post("/delete-prf-submission", storeBapComboKeys, (req, res) => { deletePRFSubmission({ rebateYear, req, res }); }); -// --- get user's 2022 CRF submissions from Formio +// --- get user's 2023 CRF submissions from Formio router.get("/crf-submissions", storeBapComboKeys, (req, res) => { // TODO res.json([]); }); -// --- post a new 2022 CRF submission to Formio +// --- post a new 2023 CRF submission to Formio + +// --- get an existing 2023 CRF's schema and submission data from Formio -// --- get an existing 2022 CRF's schema and submission data from Formio +// --- post an update to an existing draft 2023 CRF submission to Formio -// --- post an update to an existing draft 2022 CRF submission to Formio +// --- get user's 2023 Change Request form submissions from Formio +router.get("/change-submissions", storeBapComboKeys, (req, res) => { + fetchChangeSubmissions({ rebateYear, req, res }); +}); + +// --- post a new 2023 Change Request form submission to Formio +router.post("/change-submission", storeBapComboKeys, (req, res) => { + createChangeSubmission({ rebateYear, req, res }); +}); module.exports = router; diff --git a/app/server/app/utilities/formio.js b/app/server/app/utilities/formio.js index fe589d6e..f1015189 100644 --- a/app/server/app/utilities/formio.js +++ b/app/server/app/utilities/formio.js @@ -1063,6 +1063,92 @@ function fetchCRFSubmissions({ rebateYear, req, res }) { }); } +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchChangeSubmissions({ rebateYear, req, res }) { + const { bapComboKeys } = req; + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const comboKeySearchParam = `&data.${comboKeyFieldName}=`; + + const formioFormUrl = formUrl[rebateYear].change; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} Change Request form.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const submissionsUrl = + `${formioFormUrl}/submission` + + `?sort=-modified` + + `&limit=1000000` + + comboKeySearchParam + + `${bapComboKeys.join(comboKeySearchParam)}`; + + axiosFormio(req) + .get(submissionsUrl) + .then((axiosRes) => axiosRes.data) + .then((submissions) => res.json(submissions)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} Change Request form submissions.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function createChangeSubmission({ rebateYear, req, res }) { + const { bapComboKeys, body } = req; + const { mail } = req.user; + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const comboKey = body.data?.[comboKeyFieldName]; + + const formioFormUrl = formUrl[rebateYear].change; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} Change Request form.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to post a new ${rebateYear} ` + + `Change Request form submission without a matching BAP combo key.`; + log({ level: "error", message: logMessage, req }); + + const errorStatus = 401; + const errorMessage = `Unauthorized.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + /** Add custom metadata to track formio submissions from wrapper. */ + body.metadata = { ...formioCSBMetadata }; + + axiosFormio(req) + .post(`${formioFormUrl}/submission`, body) + .then((axiosRes) => axiosRes.data) + .then((submission) => res.json(submission)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error posting Formio ${rebateYear} Change Request form submission.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + module.exports = { uploadS3FileMetadata, downloadS3FileMetadata, @@ -1079,4 +1165,7 @@ module.exports = { deletePRFSubmission, // fetchCRFSubmissions, + // + fetchChangeSubmissions, + createChangeSubmission, }; From 3cdc35c96b4b97cb7545ce431d2b1391ce20d9c1 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 20:33:30 -0500 Subject: [PATCH 03/38] Add custom hooks for fetching and returning change request form submissions, and update corresponding server routes and function names --- app/client/src/utilities.ts | 68 ++++++++++++++++++++++++----- app/server/app/config/formio.js | 2 +- app/server/app/routes/formio2023.js | 12 ++--- app/server/app/utilities/formio.js | 8 ++-- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/app/client/src/utilities.ts b/app/client/src/utilities.ts index 4dd16f0f..88144bd7 100644 --- a/app/client/src/utilities.ts +++ b/app/client/src/utilities.ts @@ -111,6 +111,10 @@ type BapFormSubmissions = { }; }; +type FormioChangeRequest = { + // TODO +}; + type FormioSubmission = { [field: string]: unknown; _id: string; // MongoDB ObjectId string @@ -286,7 +290,7 @@ async function fetchData(url: string, options: RequestInit) { /** * Fetches data and returns a promise containing JSON fetched from a provided - * web service URL or handles any other OK response returned from the server + * web service URL or handles any other OK response returned from the server. */ export function getData(url: string) { return fetchData(url, { @@ -297,7 +301,7 @@ export function getData(url: string) { /** * Posts JSON data and returns a promise containing JSON fetched from a provided - * web service URL or handles any other OK response returned from the server + * web service URL or handles any other OK response returned from the server. */ export function postData(url: string, data: object) { return fetchData(url, { @@ -308,7 +312,7 @@ export function postData(url: string, data: object) { }); } -/** Custom hook to fetch content data */ +/** Custom hook to fetch content data. */ export function useContentQuery() { return useQuery({ queryKey: ["content"], @@ -317,13 +321,13 @@ export function useContentQuery() { }); } -/** Custom hook that returns cached fetched content data */ +/** Custom hook that returns cached fetched content data. */ export function useContentData() { const queryClient = useQueryClient(); return queryClient.getQueryData(["content"]); } -/** Custom hook to fetch user data */ +/** Custom hook to fetch user data. */ export function useUserQuery() { return useQuery({ queryKey: ["user"], @@ -333,13 +337,13 @@ export function useUserQuery() { }); } -/** Custom hook that returns cached fetched user data */ +/** Custom hook that returns cached fetched user data. */ export function useUserData() { const queryClient = useQueryClient(); return queryClient.getQueryData(["user"]); } -/** Custom hook to check if user should have access to the helpdesk page */ +/** Custom hook to check if user should have access to the helpdesk page. */ export function useHelpdeskAccess() { const user = useUserData(); const userRoles = user?.memberof.split(",") || []; @@ -351,7 +355,7 @@ export function useHelpdeskAccess() { : "failure"; } -/** Custom hook to fetch CSB config */ +/** Custom hook to fetch CSB config. */ export function useConfigQuery() { return useQuery({ queryKey: ["config"], @@ -360,13 +364,13 @@ export function useConfigQuery() { }); } -/** Custom hook that returns cached fetched CSB config */ +/** Custom hook that returns cached fetched CSB config. */ export function useConfigData() { const queryClient = useQueryClient(); return queryClient.getQueryData(["config"]); } -/** Custom hook to fetch BAP SAM.gov data */ +/** Custom hook to fetch BAP SAM.gov data. */ export function useBapSamQuery() { return useQuery({ queryKey: ["bap/sam"], @@ -383,13 +387,53 @@ export function useBapSamQuery() { }); } -/** Custom hook that returns cached fetched BAP SAM.gov data */ +/** Custom hook that returns cached fetched BAP SAM.gov data. */ export function useBapSamData() { const queryClient = useQueryClient(); return queryClient.getQueryData(["bap/sam"]); } -/** Custom hook to fetch submissions from the BAP and Formio */ +/** Custom hook to fetch Change Request form submissions from Formio. */ +export function useChangeRequestsQuery(rebateYear: RebateYear) { + /* NOTE: Change Request form was added in the 2023 rebate year */ + const changeRequest2022Query = { + queryKey: ["formio/2022/change-requests"], + queryFn: () => { + return Promise.resolve([] as FormioChangeRequest[]); + }, + refetchOnWindowFocus: false, + }; + + const changeRequest2023Query = { + queryKey: ["formio/2023/change-requests"], + queryFn: () => { + const url = `${serverUrl}/api/formio/2023/change-requests`; + return getData(url); + }, + refetchOnWindowFocus: false, + }; + + return rebateYear === "2022" + ? changeRequest2022Query + : rebateYear === "2023" + ? changeRequest2023Query + : undefined; +} + +/** + * Custom hook that returns cached fetched Change Request form submissions from + * Formio. + */ +export function useChangeRequestsData(rebateYear: RebateYear) { + const queryClient = useQueryClient(); + return rebateYear === "2022" + ? queryClient.getQueryData(["formio/2022/change-requests"]) // prettier-ignore + : rebateYear === "2023" + ? queryClient.getQueryData(["formio/2023/change-requests"]) // prettier-ignore + : undefined; +} + +/** Custom hook to fetch submissions from the BAP and Formio. */ export function useSubmissionsQueries(rebateYear: RebateYear) { const bapQuery = { queryKey: ["bap/submissions"], diff --git a/app/server/app/config/formio.js b/app/server/app/config/formio.js index e3ed17ac..2d89b5e8 100644 --- a/app/server/app/config/formio.js +++ b/app/server/app/config/formio.js @@ -34,7 +34,7 @@ const formUrl = { frf: `${formioProjectUrl}/${FORMIO_2022_FRF_PATH}`, prf: `${formioProjectUrl}/${FORMIO_2022_PRF_PATH}`, crf: `${formioProjectUrl}/${FORMIO_2022_CRF_PATH}`, - change: "", // NOTE: change request form was added in the 2023 rebate year + change: "", // NOTE: Change Request form was added in the 2023 rebate year }, 2023: { frf: `${formioProjectUrl}/${FORMIO_2023_FRF_PATH}`, diff --git a/app/server/app/routes/formio2023.js b/app/server/app/routes/formio2023.js index 7bb5c595..616be550 100644 --- a/app/server/app/routes/formio2023.js +++ b/app/server/app/routes/formio2023.js @@ -20,8 +20,8 @@ const { updatePRFSubmission, deletePRFSubmission, // - fetchChangeSubmissions, - createChangeSubmission, + fetchChangeRequests, + createChangeRequest, } = require("../utilities/formio"); const rebateYear = "2023"; @@ -115,13 +115,13 @@ router.get("/crf-submissions", storeBapComboKeys, (req, res) => { // --- post an update to an existing draft 2023 CRF submission to Formio // --- get user's 2023 Change Request form submissions from Formio -router.get("/change-submissions", storeBapComboKeys, (req, res) => { - fetchChangeSubmissions({ rebateYear, req, res }); +router.get("/change-requests", storeBapComboKeys, (req, res) => { + fetchChangeRequests({ rebateYear, req, res }); }); // --- post a new 2023 Change Request form submission to Formio -router.post("/change-submission", storeBapComboKeys, (req, res) => { - createChangeSubmission({ rebateYear, req, res }); +router.post("/change-requests", storeBapComboKeys, (req, res) => { + createChangeRequest({ rebateYear, req, res }); }); module.exports = router; diff --git a/app/server/app/utilities/formio.js b/app/server/app/utilities/formio.js index f1015189..d64f0019 100644 --- a/app/server/app/utilities/formio.js +++ b/app/server/app/utilities/formio.js @@ -1069,7 +1069,7 @@ function fetchCRFSubmissions({ rebateYear, req, res }) { * @param {express.Request} param.req * @param {express.Response} param.res */ -function fetchChangeSubmissions({ rebateYear, req, res }) { +function fetchChangeRequests({ rebateYear, req, res }) { const { bapComboKeys } = req; const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); @@ -1108,7 +1108,7 @@ function fetchChangeSubmissions({ rebateYear, req, res }) { * @param {express.Request} param.req * @param {express.Response} param.res */ -function createChangeSubmission({ rebateYear, req, res }) { +function createChangeRequest({ rebateYear, req, res }) { const { bapComboKeys, body } = req; const { mail } = req.user; @@ -1166,6 +1166,6 @@ module.exports = { // fetchCRFSubmissions, // - fetchChangeSubmissions, - createChangeSubmission, + fetchChangeRequests, + createChangeRequest, }; From 69c0449ab2458903b15e56ec03b59f0485777260 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 20:56:54 -0500 Subject: [PATCH 04/38] Update useChangeRequestsQuery() to return the query result and update Submissions2023 component to use the query and log the results --- app/client/src/routes/submissions.tsx | 16 +++++++++++++-- app/client/src/utilities.ts | 29 ++++++++++++++++++--------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 5be128f2..199e0145 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -21,6 +21,8 @@ import { useContentData, useConfigData, useBapSamData, + useChangeRequestsQuery, + useChangeRequestsData, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -1376,14 +1378,24 @@ function Submissions2022() { function Submissions2023() { const content = useContentData(); + const changeRequestsQuery = useChangeRequestsQuery("2023"); const submissionsQueries = useSubmissionsQueries("2023"); + const changeRequests = useChangeRequestsData("2023"); const submissions = useSubmissions("2023"); - if (submissionsQueries.some((query) => query.isFetching)) { + console.log(changeRequests); // TODO: display change requests table if there are any + + if ( + changeRequestsQuery.isFetching || + submissionsQueries.some((query) => query.isFetching) + ) { return ; } - if (submissionsQueries.some((query) => query.isError)) { + if ( + changeRequestsQuery.isError || + submissionsQueries.some((query) => query.isError) + ) { return ; } diff --git a/app/client/src/utilities.ts b/app/client/src/utilities.ts index 88144bd7..f693e750 100644 --- a/app/client/src/utilities.ts +++ b/app/client/src/utilities.ts @@ -395,12 +395,13 @@ export function useBapSamData() { /** Custom hook to fetch Change Request form submissions from Formio. */ export function useChangeRequestsQuery(rebateYear: RebateYear) { - /* NOTE: Change Request form was added in the 2023 rebate year */ + /* + * NOTE: Change Request form was added in the 2023 rebate year, so there's no + * change request data to fetch for 2022. + */ const changeRequest2022Query = { queryKey: ["formio/2022/change-requests"], - queryFn: () => { - return Promise.resolve([] as FormioChangeRequest[]); - }, + queryFn: () => Promise.resolve([]), refetchOnWindowFocus: false, }; @@ -413,11 +414,21 @@ export function useChangeRequestsQuery(rebateYear: RebateYear) { refetchOnWindowFocus: false, }; - return rebateYear === "2022" - ? changeRequest2022Query - : rebateYear === "2023" - ? changeRequest2023Query - : undefined; + /* NOTE: Fallback (not used, as rebate year will match a query above) */ + const changeRequestQuery = { + queryKey: ["formio/change-requests"], + queryFn: () => Promise.resolve([]), + refetchOnWindowFocus: false, + }; + + const query = + rebateYear === "2022" + ? changeRequest2022Query + : rebateYear === "2023" + ? changeRequest2023Query + : changeRequestQuery; + + return useQuery(query); } /** From dd0128cefe6e5bcd6f936735ef3483e9fec29216 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 21:32:41 -0500 Subject: [PATCH 05/38] Add change request column and button link to 2023 table of submissions --- app/client/src/routes/submissions.tsx | 71 ++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 199e0145..28da4b3b 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -43,15 +43,16 @@ const defaultTableRowClassNames = "bg-gray-5"; const highlightedTableRowClassNames = "bg-primary-lighter"; function FormButtonLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { - const icon = props.type === "edit" ? "edit" : "visibility"; - const text = props.type === "edit" ? "Edit" : "View"; + const { type, to } = props; + const icon = type === "edit" ? "edit" : "visibility"; + const text = type === "edit" ? "Edit" : "View"; return ( @@ -70,6 +71,37 @@ function FormButtonLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { ); } +function ChangeRequestTextIcon() { + return ( + + Change + + + ); +} + +function ChangeRequestButtonLink(props: { + to: LinkProps["to"]; + disabled: boolean; +}) { + const { to, disabled } = props; + + const btnClassNames = + "usa-button margin-0 padding-x-2 padding-y-1 font-sans-2xs"; + + return disabled ? ( + + ) : ( + + + + ); +} + function NewApplicationIconText() { return ( @@ -81,7 +113,9 @@ function NewApplicationIconText() { ); } -function SubmissionsTableHeader() { +function SubmissionsTableHeader(props: { rebateYear: RebateYear }) { + const { rebateYear } = props; + return ( @@ -140,6 +174,15 @@ function SubmissionsTableHeader() { tooltip="Last date this form was updated" /> + + {rebateYear === "2023" && ( + + + + )} ); @@ -1063,6 +1106,13 @@ function FRF2023Submission(props: { rebate: Rebate }) {
{date} + + + + ); } @@ -1307,6 +1357,13 @@ function PRF2023Submission(props: { rebate: Rebate }) {
{date} + + + + ); } @@ -1350,7 +1407,7 @@ function Submissions2022() { aria-label="Your 2022 Rebate Forms" className="usa-table usa-table--stacked usa-table--borderless width-full" > - + {submissions.map((rebate, index) => { return rebate.rebateYear === "2022" ? ( @@ -1421,7 +1478,7 @@ function Submissions2023() { aria-label="Your 2023 Rebate Forms" className="usa-table usa-table--stacked usa-table--borderless width-full" > - + {submissions.map((rebate, index) => { return rebate.rebateYear === "2023" ? ( From 99edba53d64520d82a6f6e2973b8b4cec2e7a2b9 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 21:35:15 -0500 Subject: [PATCH 06/38] Simplify naming of button link components in Submissions component --- app/client/src/routes/submissions.tsx | 51 +++++++++++---------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 28da4b3b..b6b6430e 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -42,7 +42,7 @@ import { const defaultTableRowClassNames = "bg-gray-5"; const highlightedTableRowClassNames = "bg-primary-lighter"; -function FormButtonLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { +function FormLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { const { type, to } = props; const icon = type === "edit" ? "edit" : "visibility"; const text = type === "edit" ? "Edit" : "View"; @@ -82,10 +82,7 @@ function ChangeRequestTextIcon() { ); } -function ChangeRequestButtonLink(props: { - to: LinkProps["to"]; - disabled: boolean; -}) { +function ChangeRequestLink(props: { to: LinkProps["to"]; disabled: boolean }) { const { to, disabled } = props; const btnClassNames = @@ -290,11 +287,11 @@ function FRF2022Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : frf.formio.state === "submitted" || !frfSubmissionPeriodOpen ? ( - + ) : frf.formio.state === "draft" ? ( - + ) : null} @@ -624,13 +621,13 @@ function PRF2022Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : prfNeedsEdits ? ( - + ) : prf.formio.state === "submitted" || !prfSubmissionPeriodOpen ? ( - + ) : prf.formio.state === "draft" ? ( - + ) : null} @@ -854,11 +851,11 @@ function CRF2022Submission(props: { rebate: Rebate }) { > {crfNeedsEdits ? ( - + ) : crf.formio.state === "submitted" || !crfSubmissionPeriodOpen ? ( - + ) : crf.formio.state === "draft" ? ( - + ) : null} @@ -1010,11 +1007,11 @@ function FRF2023Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : frf.formio.state === "submitted" || !frfSubmissionPeriodOpen ? ( - + ) : frf.formio.state === "draft" ? ( - + ) : null} @@ -1108,10 +1105,7 @@ function FRF2023Submission(props: { rebate: Rebate }) { - + ); @@ -1310,13 +1304,13 @@ function PRF2023Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : prfNeedsEdits ? ( - + ) : prf.formio.state === "submitted" || !prfSubmissionPeriodOpen ? ( - + ) : prf.formio.state === "draft" ? ( - + ) : null} @@ -1359,10 +1353,7 @@ function PRF2023Submission(props: { rebate: Rebate }) { - + ); From 9188e9db18ff0887a933288831c2dc38182e6d15 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 23:07:25 -0500 Subject: [PATCH 07/38] Update ChangeRequestButton to post data when clicked, and update LoadingButtonIcon to take a position prop, so the correct margin can be set (left or right), making the component more flexible --- app/client/src/components/loading.tsx | 10 +- app/client/src/routes/frfNew.tsx | 2 +- app/client/src/routes/helpdesk.tsx | 3 +- app/client/src/routes/submissions.tsx | 181 +++++++++++++++++++++----- app/client/src/utilities.ts | 26 +++- 5 files changed, 175 insertions(+), 47 deletions(-) diff --git a/app/client/src/components/loading.tsx b/app/client/src/components/loading.tsx index 952f9691..fa55f31e 100644 --- a/app/client/src/components/loading.tsx +++ b/app/client/src/components/loading.tsx @@ -1,3 +1,5 @@ +import clsx from "clsx"; +// --- // NOTE: React JSX doesn't support namespaces, so `uswds/img/loader.svg` copied // into app's `images/loader.svg` with namespace tags removed import loader from "@/images/loader.svg"; @@ -12,12 +14,16 @@ export function Loading() { ); } -export function LoadingButtonIcon() { +export function LoadingButtonIcon(props: { position: "start" | "end" }) { + const { position } = props; return ( Loading... ); } diff --git a/app/client/src/routes/frfNew.tsx b/app/client/src/routes/frfNew.tsx index a76e1e3e..e918e843 100644 --- a/app/client/src/routes/frfNew.tsx +++ b/app/client/src/routes/frfNew.tsx @@ -347,7 +347,7 @@ export function FRFNew() { New Application
{postingDataId === comboKey && ( - + )} diff --git a/app/client/src/routes/helpdesk.tsx b/app/client/src/routes/helpdesk.tsx index 42bee296..2ce12296 100644 --- a/app/client/src/routes/helpdesk.tsx +++ b/app/client/src/routes/helpdesk.tsx @@ -15,6 +15,7 @@ import { bapCRFStatusMap, } from "@/config"; import { + type FormType, type FormioFRF2022Submission, type FormioPRF2022Submission, type FormioCRF2022Submission, @@ -36,8 +37,6 @@ import { useRebateYearActions, } from "@/contexts/rebateYear"; -type FormType = "frf" | "prf" | "crf"; - type ServerResponse = | { formSchema: null; diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index b6b6430e..d687d6f1 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -10,6 +10,8 @@ import icons from "uswds/img/sprite.svg"; // --- import { serverUrl, messages } from "@/config"; import { + type FormType, + type FormioChangeRequest2023Submission, type FormioFRF2022Submission, type FormioPRF2022Submission, type FormioCRF2022Submission, @@ -71,31 +73,99 @@ function FormLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { ); } -function ChangeRequestTextIcon() { - return ( - - Change - - - ); -} +function ChangeRequestButton(props: { + data: { + formType: FormType; + comboKey: string; + mongoId: string; + rebateId: string; + email: string; + title: string; + name: string; + }; + disabled: boolean; +}) { + const { data, disabled } = props; + const { formType, comboKey, mongoId, rebateId, email, title, name } = data; + + const navigate = useNavigate(); -function ChangeRequestLink(props: { to: LinkProps["to"]; disabled: boolean }) { - const { to, disabled } = props; + const { displayErrorNotification } = useNotificationsActions(); - const btnClassNames = - "usa-button margin-0 padding-x-2 padding-y-1 font-sans-2xs"; + /** + * Stores when data is being posted to the server, so a loading indicator can + * be rendered inside the "Change" button, and we can prevent double submits/ + * creations of new Change Request form submissions. + */ + const [dataIsPosting, setDataIsPosting] = useState(false); - return disabled ? ( - - ) : ( - - - ); } @@ -532,7 +602,7 @@ function PRF2022Submission(props: { rebate: Rebate }) { New Payment Request - {dataIsPosting && } + {dataIsPosting && } @@ -776,7 +846,7 @@ function CRF2022Submission(props: { rebate: Rebate }) { New Close Out - {dataIsPosting && } + {dataIsPosting && } @@ -910,9 +980,24 @@ function FRF2023Submission(props: { rebate: Rebate }) { const { rebate } = props; const { frf, prf, crf } = rebate; + const { email } = useOutletContext<{ email: string }>(); + const configData = useConfigData(); + const bapSamData = useBapSamData(); - if (!configData) return null; + if (!configData || !bapSamData) return null; + + /** matched SAM.gov entity for the FRF submission */ + const entity = bapSamData.entities.find((entity) => { + const { ENTITY_STATUS__c, ENTITY_COMBO_KEY__c } = entity; + const comboKey = (frf.formio as FormioFRF2023Submission).data + ._bap_entity_combo_key; + return ENTITY_STATUS__c === "Active" && ENTITY_COMBO_KEY__c === comboKey; + }); + + if (!entity) return null; + + const { title, name } = getUserInfo(email, entity); const frfSubmissionPeriodOpen = configData.submissionPeriodOpen["2023"].frf; @@ -1105,7 +1190,18 @@ function FRF2023Submission(props: { rebate: Rebate }) { - + ); @@ -1131,12 +1227,6 @@ function PRF2023Submission(props: { rebate: Rebate }) { if (!configData || !bapSamData) return null; - const prfSubmissionPeriodOpen = configData.submissionPeriodOpen["2023"].prf; - - const frfSelected = frf.bap?.status === "Accepted"; - - const frfSelectedButNoPRF = frfSelected && !Boolean(prf.formio); - /** matched SAM.gov entity for the FRF submission */ const entity = bapSamData.entities.find((entity) => { const { ENTITY_STATUS__c, ENTITY_COMBO_KEY__c } = entity; @@ -1145,6 +1235,16 @@ function PRF2023Submission(props: { rebate: Rebate }) { return ENTITY_STATUS__c === "Active" && ENTITY_COMBO_KEY__c === comboKey; }); + if (!entity) return null; + + const { title, name } = getUserInfo(email, entity); + + const prfSubmissionPeriodOpen = configData.submissionPeriodOpen["2023"].prf; + + const frfSelected = frf.bap?.status === "Accepted"; + + const frfSelectedButNoPRF = frfSelected && !Boolean(prf.formio); + if (frfSelectedButNoPRF) { return ( @@ -1154,14 +1254,12 @@ function PRF2023Submission(props: { rebate: Rebate }) { disabled={!prfSubmissionPeriodOpen} onClick={(_ev) => { if (!prfSubmissionPeriodOpen) return; - if (!frf.bap || !entity) return; + if (!frf.bap) return; // account for when data is posting to prevent double submits if (dataIsPosting) return; setDataIsPosting(true); - const { title, name } = getUserInfo(email, entity); - // create a new draft PRF submission postData(`${serverUrl}/api/formio/2023/prf-submission/`, { email, @@ -1215,7 +1313,7 @@ function PRF2023Submission(props: { rebate: Rebate }) { New Payment Request - {dataIsPosting && } + {dataIsPosting && } @@ -1353,7 +1451,18 @@ function PRF2023Submission(props: { rebate: Rebate }) { - + ); diff --git a/app/client/src/utilities.ts b/app/client/src/utilities.ts index f693e750..382ee594 100644 --- a/app/client/src/utilities.ts +++ b/app/client/src/utilities.ts @@ -111,9 +111,7 @@ type BapFormSubmissions = { }; }; -type FormioChangeRequest = { - // TODO -}; +export type FormType = "frf" | "prf" | "crf"; type FormioSubmission = { [field: string]: unknown; @@ -128,6 +126,18 @@ type FormioSubmission = { }; }; +type FormioChangeRequest2023Data = { + [field: string]: unknown; + // fields injected upon a new draft Change Request form submission creation: + _bap_entity_combo_key: string; + _mongo_id: string; + _rebate_id: string; + _request_form: FormType; + _user_email: string; + _user_title: string; + _user_name: string; +}; + type FormioFRF2022Data = { [field: string]: unknown; // fields injected upon a new draft FRF submission creation: @@ -226,6 +236,10 @@ type FormioCRF2023Data = { _bap_rebate_id: string; }; +export type FormioChangeRequest2023Submission = FormioSubmission & { + data: FormioChangeRequest2023Data; +}; + export type FormioFRF2022Submission = FormioSubmission & { data: FormioFRF2022Data; }; @@ -409,7 +423,7 @@ export function useChangeRequestsQuery(rebateYear: RebateYear) { queryKey: ["formio/2023/change-requests"], queryFn: () => { const url = `${serverUrl}/api/formio/2023/change-requests`; - return getData(url); + return getData(url); }, refetchOnWindowFocus: false, }; @@ -438,9 +452,9 @@ export function useChangeRequestsQuery(rebateYear: RebateYear) { export function useChangeRequestsData(rebateYear: RebateYear) { const queryClient = useQueryClient(); return rebateYear === "2022" - ? queryClient.getQueryData(["formio/2022/change-requests"]) // prettier-ignore + ? queryClient.getQueryData<[]>(["formio/2022/change-requests"]) : rebateYear === "2023" - ? queryClient.getQueryData(["formio/2023/change-requests"]) // prettier-ignore + ? queryClient.getQueryData(["formio/2023/change-requests"]) // prettier-ignore : undefined; } From c0384fdc7c29832cc74d7eacc80aa08b87ca43d9 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 29 Jan 2024 23:47:09 -0500 Subject: [PATCH 08/38] Update use of ChangeRequestButton to pass required data (e.g. comboKey, mongoId, and rebateId) --- app/client/src/routes/submissions.tsx | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index d687d6f1..9a413fd9 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -74,18 +74,18 @@ function FormLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { } function ChangeRequestButton(props: { + disabled: boolean; data: { formType: FormType; comboKey: string; mongoId: string; - rebateId: string; + rebateId: string | null; email: string; title: string; name: string; }; - disabled: boolean; }) { - const { data, disabled } = props; + const { disabled, data } = props; const { formType, comboKey, mongoId, rebateId, email, title, name } = data; const navigate = useNavigate(); @@ -102,9 +102,9 @@ function ChangeRequestButton(props: { return ( + ); +} diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 69c44b70..79112aa0 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -11,8 +11,6 @@ import icons from "uswds/img/sprite.svg"; // --- import { serverUrl, messages } from "@/config"; import { - type FormType, - type FormioChange2023Submission, type FormioFRF2022Submission, type FormioPRF2022Submission, type FormioCRF2022Submission, @@ -35,6 +33,7 @@ import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { TextWithTooltip } from "@/components/tooltip"; +import { New2023ChangeRequest } from "@/components/change2023New"; import { useNotificationsActions } from "@/contexts/notifications"; import { type RebateYear, @@ -74,112 +73,6 @@ function FormLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { ); } -function ChangeRequestButton(props: { - disabled: boolean; - data: { - formType: FormType; - comboKey: string; - mongoId: string; - rebateId: string | null; - email: string; - title: string; - name: string; - }; -}) { - const { disabled, data } = props; - const { formType, comboKey, mongoId, rebateId, email, title, name } = data; - - const navigate = useNavigate(); - - const { displayErrorNotification } = useNotificationsActions(); - - /** - * Stores when data is being posted to the server, so a loading indicator can - * be rendered inside the "Change" button, and we can prevent double submits/ - * creations of new Change Request form submissions. - */ - const [dataIsPosting, setDataIsPosting] = useState(false); - - return ( - - ); -} - function NewApplicationIconText() { return ( @@ -1202,7 +1095,7 @@ function FRF2023Submission(props: { rebate: Rebate }) { - - Date: Fri, 9 Feb 2024 17:14:28 -0500 Subject: [PATCH 29/38] Create modal to display change request 2023 form --- app/client/src/components/change2023New.tsx | 208 +++++++++++++------- app/client/src/routes/submissions.tsx | 6 +- 2 files changed, 135 insertions(+), 79 deletions(-) diff --git a/app/client/src/components/change2023New.tsx b/app/client/src/components/change2023New.tsx index a58c70c1..b06ced6a 100644 --- a/app/client/src/components/change2023New.tsx +++ b/app/client/src/components/change2023New.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { type Dispatch, type SetStateAction, Fragment, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; import icons from "uswds/img/sprite.svg"; // --- @@ -9,10 +10,9 @@ import { type FormioChange2023Submission, postData, } from "@/utilities"; -import { LoadingButtonIcon } from "@/components/loading"; import { useNotificationsActions } from "@/contexts/notifications"; -export function New2023ChangeRequest(props: { +export function ChangeRequest2023Button(props: { disabled: boolean; data: { formType: FormType; @@ -27,38 +27,24 @@ export function New2023ChangeRequest(props: { const { disabled, data } = props; const { formType, comboKey, mongoId, rebateId, email, title, name } = data; - const navigate = useNavigate(); - - const { displayErrorNotification } = useNotificationsActions(); - - /** - * Stores when data is being posted to the server, so a loading indicator can - * be rendered inside the "Change" button, and we can prevent double submits/ - * creations of new Change Request form submissions. - */ - const [dataIsPosting, setDataIsPosting] = useState(false); + const [dialogShown, setDialogShown] = useState(false); return ( - + + + + ); +} + +function ChangeRequest2023Dialog(props: { + dialogShown: boolean; + setDialogShown: Dispatch>; +}) { + const { dialogShown, setDialogShown } = props; + + return ( + + setDialogShown(false)} + > + +
+ + +
+
+ + +
+
- Error creating Change Request for{" "} - - {formType.toUpperCase()} {rebateId} - - . -

-

- Please try again. -

- - ), - }); - }) - .finally(() => { - setDataIsPosting(false); - }); - }} - > - - {dataIsPosting && } - Change - - - + +
+
+ +
+ +
+
+
+
+
+
+
+ ); +} + +function ChangeRequest2023Form() { + return ( + <> +

Change Request Form!

+ ); } diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 79112aa0..d2227224 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -33,7 +33,7 @@ import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { TextWithTooltip } from "@/components/tooltip"; -import { New2023ChangeRequest } from "@/components/change2023New"; +import { ChangeRequest2023Button } from "@/components/change2023New"; import { useNotificationsActions } from "@/contexts/notifications"; import { type RebateYear, @@ -1095,7 +1095,7 @@ function FRF2023Submission(props: { rebate: Rebate }) { - - Date: Fri, 9 Feb 2024 22:04:28 -0500 Subject: [PATCH 30/38] Add server routes and logic to fetch 2023 change request form schema --- app/server/app/routes/formio2023.js | 6 ++++++ app/server/app/utilities/formio.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/server/app/routes/formio2023.js b/app/server/app/routes/formio2023.js index 57d051bb..d23f559b 100644 --- a/app/server/app/routes/formio2023.js +++ b/app/server/app/routes/formio2023.js @@ -21,6 +21,7 @@ const { deletePRFSubmission, // fetchChangeRequests, + fetchChangeRequestSchema, createChangeRequest, fetchChangeRequest, updateChangeRequest, @@ -121,6 +122,11 @@ router.get("/changes", storeBapComboKeys, (req, res) => { fetchChangeRequests({ rebateYear, req, res }); }); +// --- get the 2023 Change Request form schema from Formio +router.get("/change", storeBapComboKeys, (req, res) => { + fetchChangeRequestSchema({ rebateYear, req, res }); +}); + // --- post a new 2023 Change Request form submission to Formio router.post("/change", storeBapComboKeys, (req, res) => { createChangeRequest({ rebateYear, req, res }); diff --git a/app/server/app/utilities/formio.js b/app/server/app/utilities/formio.js index 473abf75..619a0c80 100644 --- a/app/server/app/utilities/formio.js +++ b/app/server/app/utilities/formio.js @@ -1102,6 +1102,33 @@ function fetchChangeRequests({ rebateYear, req, res }) { }); } +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchChangeRequestSchema({ rebateYear, req, res }) { + const formioFormUrl = formUrl[rebateYear].change; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} Change Request form.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + axiosFormio(req) + .get(formioFormUrl) + .then((axiosRes) => axiosRes.data) + .then((schema) => res.json({ url: formioFormUrl, json: schema })) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} Change Request form schema.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + /** * @param {Object} param * @param {'2022' | '2023'} param.rebateYear @@ -1275,6 +1302,7 @@ module.exports = { fetchCRFSubmissions, // fetchChangeRequests, + fetchChangeRequestSchema, createChangeRequest, fetchChangeRequest, updateChangeRequest, From 3e023e5076c318a563a18bfcc776b8e572946df2 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Fri, 9 Feb 2024 22:05:35 -0500 Subject: [PATCH 31/38] Update ChangeRequest2023Form to fetch form schema, and display form in modal dialog --- app/client/src/components/change2023New.tsx | 209 ++++++++++++++++---- app/client/src/config.tsx | 1 + 2 files changed, 177 insertions(+), 33 deletions(-) diff --git a/app/client/src/components/change2023New.tsx b/app/client/src/components/change2023New.tsx index b06ced6a..f0e807e3 100644 --- a/app/client/src/components/change2023New.tsx +++ b/app/client/src/components/change2023New.tsx @@ -1,34 +1,67 @@ -import { type Dispatch, type SetStateAction, Fragment, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { useQueryClient, useQuery } from "@tanstack/react-query"; import { Dialog, Transition } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; +import { Form } from "@formio/react"; import clsx from "clsx"; import icons from "uswds/img/sprite.svg"; // --- -import { serverUrl } from "@/config"; +import { serverUrl, messages } from "@/config"; import { type FormType, type FormioChange2023Submission, + getData, postData, + useContentData, } from "@/utilities"; +import { Loading } from "@/components/loading"; +import { Message } from "@/components/message"; +import { MarkdownContent } from "@/components/markdownContent"; import { useNotificationsActions } from "@/contexts/notifications"; +type ChangeRequestData = { + formType: FormType; + comboKey: string; + mongoId: string; + rebateId: string | null; + email: string; + title: string; + name: string; +}; + +type ServerResponse = { url: string; json: object }; + +/** Custom hook to fetch Formio schema */ +function useFormioSchemaQuery() { + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.resetQueries({ queryKey: ["formio/2023/change"] }); + }, [queryClient]); + + const url = `${serverUrl}/api/formio/2023/change`; + + const query = useQuery({ + queryKey: ["formio/2023/change"], + queryFn: () => getData(url), + refetchOnWindowFocus: false, + }); + + return { query }; +} + export function ChangeRequest2023Button(props: { disabled: boolean; - data: { - formType: FormType; - comboKey: string; - mongoId: string; - rebateId: string | null; - email: string; - title: string; - name: string; - }; + data: ChangeRequestData; }) { const { disabled, data } = props; - const { formType, comboKey, mongoId, rebateId, email, title, name } = data; const [dialogShown, setDialogShown] = useState(false); + function closeDialog() { + setDialogShown(false); + } + return ( <>