diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index d5dc3dd7bc..2d05397f5c 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -2,7 +2,6 @@ name: GitHub Pages on: - # Runs on pushes targeting the default branch push: branches: ["master"] @@ -23,6 +22,9 @@ concurrency: group: "pages" cancel-in-progress: true +env: + STAGE_NAME: ${{ startsWith(github.ref_name, 'snyk-') && 'snyk' || github.ref_name }} + jobs: # Build job build: @@ -32,15 +34,28 @@ jobs: uses: actions/checkout@v3 - name: Setup Pages uses: actions/configure-pages@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v3.5.1 + - uses: ./.github/actions/setup + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.AWS_OIDC_ROLE_TO_ASSUME }} + aws-region: us-east-1 + role-duration-seconds: 10800 + - name: Set env + run: | + echo "NEXT_PUBLIC_API_REST_URL=$( + aws cloudformation \ + --region us-east-1 describe-stacks \ + --stack-name $PROJECT-api-$STAGE_NAME | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "ApiGatewayRestApiUrl") | .OutputValue' + )" >> $GITHUB_ENV - name: Install Packages run: | cd docs/_deploy-metrics rm -rf node_modules yarn install --frozen-lockfile echo $BRANCHES_TO_GENERATE - - name: Build Dora + - name: Build Deploy Metrics run: | cd docs/_deploy-metrics yarn build @@ -48,7 +63,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} REPO_NAME: macpro-om-template - BRANCHES_TO_GENERATE: master + BRANCHES_TO_GENERATE: master,val,production - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 with: diff --git a/.gitignore b/.gitignore index b6925ba786..0be21c7dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ build_run .build .turbo coverage -lambda_layer.zip \ No newline at end of file +lambda_layer.zip +**/.auth/*.json \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml index 000638a669..5fbc404476 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -39,14 +39,22 @@ contact_email: bpaige@gswell.com team: members: # This list automatically populates the Team Introduction page. Add/Edit as appropriate. - - role: Product Owner + - role: Product description: Responsible for project scope, direction, and delivery. - name: Anna Hawk - email: ahawk@gswell.com + name: Hannah Morris + email: hmorris@gswell.com + - role: Product + description: Responsible for project scope, direction, and delivery. + name: Erika Durant + email: edurant@fearless.tech - role: Tech Lead - description: Leads tooling, tech, and arch discussions and decisions. + description: Tooling, tech, and arch discussions and decisions. name: Ben Paige email: bpaige@gswell.com + - role: Tech Lead + description: Tooling, tech, and arch discussions and decisions. + name: Michael Dial + email: mdial@gswell.com core_hours: 10:00am - 3:00pm ET meetings: diff --git a/docs/_deploy-metrics/lib/formData.ts b/docs/_deploy-metrics/lib/formData.ts new file mode 100644 index 0000000000..bccd8ce395 --- /dev/null +++ b/docs/_deploy-metrics/lib/formData.ts @@ -0,0 +1,92 @@ +export type FormResult = { + version: string; + data: any; // replace 'any' with the actual type of the data returned from the API + } | null + + export type ResultObject = { + [formId: string]: FormResult[]; + } + + export async function getAllFormData(formData: any): Promise { + const resultObject: ResultObject = {}; + + // Loop through each key-value pair in formData + for (const formId in formData) { + if (formData.hasOwnProperty(formId)) { + const formVersions = formData[formId]; + + // Loop through each formVersion for the current formId + resultObject[formId] = await Promise.all( + formVersions.map(async (formVersion: any) => { + try { + // Make API request using fetch + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_REST_URL}/forms?formId=${formId.toLowerCase()}&formVersion=${formVersion}` + ); + + // Ensure the request was successful + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + // Extract and format data from the API response + const data = await response.json(); + const formattedResult: FormResult = { + version: formVersion, + data, + }; + + return formattedResult; + } catch (error) { + // Handle error if API request fails + console.error(`Error fetching data for formId: ${formId}, version: ${formVersion}`); + console.error(error); + return null; + } + }) + ); + } + } + + return resultObject; + } + +export function generateDocs(obj: any, results: any = [], parentName: string = '', prompt: string = '') { + if (typeof obj === 'object' && obj !== null) { + if ('rhf' in obj) { + const resultItem: any = { rhf: obj.rhf }; + + if ('label' in obj) { + resultItem.label = obj.label; + } + + if ('name' in obj) { + resultItem.name = obj.name; + } + + if ((obj.rhf === 'Select' || obj.rhf === 'Radio') && obj.props) { + resultItem.options = [] + obj.props?.options.forEach((field: any) => { + resultItem.options?.push(field.value) + }) + } + + resultItem.parentName = parentName; + resultItem.prompt = prompt; + + results.push(resultItem); + } + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if ('name' in obj) { + parentName = obj.name; + } + if ('description' in obj) { + prompt = obj.description; + } + generateDocs(obj[key], results, parentName, prompt); + } + } + } +} diff --git a/docs/_deploy-metrics/package.json b/docs/_deploy-metrics/package.json index a1169290fd..a0497d6f9a 100644 --- a/docs/_deploy-metrics/package.json +++ b/docs/_deploy-metrics/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@chakra-ui/react": "^2.3.6", - "@emotion/react": "^11", + "@emotion/react": "^11.11.3", "@emotion/styled": "^11", "@octokit/auth-action": "^2.0.2", "@octokit/types": "^8.0.0", @@ -25,11 +25,11 @@ "react-dom": "18.2.0", "react-icons": "^4.8.0", "react-json-to-csv": "^1.2.0", - "recharts": "^2.10.3" + "recharts": "^2.10.4" }, "devDependencies": { "@types/node": "18.11.0", - "@types/react": "18.0.21", + "@types/react": "^18.2.21", "@types/react-dom": "18.0.6", "eslint": "8.25.0", "eslint-config-next": "12.3.1", diff --git a/docs/_deploy-metrics/pages/webforms/index.tsx b/docs/_deploy-metrics/pages/webforms/index.tsx new file mode 100644 index 0000000000..9d4b1155d3 --- /dev/null +++ b/docs/_deploy-metrics/pages/webforms/index.tsx @@ -0,0 +1,134 @@ +import type { InferGetStaticPropsType } from "next"; +import { + Box, + HStack, + Heading, + Select, + Stack, + Text, +} from "@chakra-ui/react"; +import { + getAllFormData, + ResultObject, + generateDocs, +} from "../../lib/formData"; +import React from "react"; + +export const getStaticProps = async () => { + let allFormsWithData, allFormsAndVersions; + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_REST_URL}/allForms` + ); + allFormsAndVersions = await response.json(); + allFormsWithData = await getAllFormData(allFormsAndVersions); + } catch (e) { + console.error(e); + } + + return { + props: { + allFormsAndVersions, + allFormsWithData, + }, + }; +}; + +const WebformsDocs = ({ + allFormsAndVersions, + allFormsWithData, +}: InferGetStaticPropsType) => { + const [form, setForm] = React.useState(""); + const [version, setVersion] = React.useState(""); + + if (!allFormsWithData) return; + + return ( + + + + + Webforms Documentation + + + The purpose of this page is provide developers and anyone who might + need to use the data collected in these forms infomation about how + the data collected from the user connects to the form schema itself. + + + Choose a form and version to see all possible fields + + + + + + + {allFormsWithData[form] && version && ( + + )} + + + + ); +}; + +const VersionDocs: React.FC<{ + allFormsWithData: ResultObject; + form: string; + version: string; +}> = ({ allFormsWithData, form, version }) => { + const selectedFormData = allFormsWithData[form].find( + (f) => f?.version === version + ); + + const resultsArray: any = [] + generateDocs(selectedFormData?.data, resultsArray); + + return <> + {selectedFormData?.data?.header} + {resultsArray.map((d: any, ind: number) => ( +
+ Name: {d.name} + Type: {d.rhf} + {d.prompt && Prompt: {d.prompt}} + {d.label && Label: {d.label}} + {d.parentName && Parent: {d.parentName}} + {d.options && options: {d.options.join(', ')}} +
+
+ ))} + +}; +export default WebformsDocs; diff --git a/docs/_deploy-metrics/yarn.lock b/docs/_deploy-metrics/yarn.lock index 14fd426a4a..0dca3bec3e 100644 --- a/docs/_deploy-metrics/yarn.lock +++ b/docs/_deploy-metrics/yarn.lock @@ -920,15 +920,15 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== -"@emotion/react@^11": - version "11.11.1" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.1.tgz#b2c36afac95b184f73b08da8c214fdf861fa4157" - integrity sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA== +"@emotion/react@^11.11.3": + version "11.11.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.3.tgz#96b855dc40a2a55f52a72f518a41db4f69c31a25" + integrity sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA== dependencies: "@babel/runtime" "^7.18.3" "@emotion/babel-plugin" "^11.11.0" "@emotion/cache" "^11.11.0" - "@emotion/serialize" "^1.1.2" + "@emotion/serialize" "^1.1.3" "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" "@emotion/utils" "^1.2.1" "@emotion/weak-memoize" "^0.3.1" @@ -945,6 +945,17 @@ "@emotion/utils" "^1.2.1" csstype "^3.0.2" +"@emotion/serialize@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" + integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + "@emotion/sheet@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" @@ -1545,10 +1556,10 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.0.21": - version "18.0.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67" - integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA== +"@types/react@^18.2.21": + version "18.2.45" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.45.tgz#253f4fac288e7e751ab3dc542000fb687422c15c" + integrity sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -3700,10 +3711,10 @@ recharts-scale@^0.4.4: dependencies: decimal.js-light "^2.4.1" -recharts@^2.10.3: - version "2.10.3" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.10.3.tgz#a5dbe219354d744701e8bbd116fe42393af92f6b" - integrity sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg== +recharts@^2.10.4: + version "2.11.0" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.11.0.tgz#af17248b6d2cb099cfd42ba93b307bf807adb456" + integrity sha512-5s+u1m5Hwxb2nh0LABkE3TS/lFqFHyWl7FnPbQhHobbQQia4ih1t3o3+ikPYr31Ns+kYe4FASIthKeKi/YYvMg== dependencies: clsx "^2.0.0" eventemitter3 "^4.0.1" diff --git a/docs/assets/diagram.svg b/docs/assets/diagram.svg index ba240422ab..83aeae02d7 100644 --- a/docs/assets/diagram.svg +++ b/docs/assets/diagram.svg @@ -1,4 +1,4 @@ - + - OktaKibana \ No newline at end of file + OktaKibana \ No newline at end of file diff --git a/docs/docs/onboarding/account-access.md b/docs/docs/onboarding/access-requests.md similarity index 54% rename from docs/docs/onboarding/account-access.md rename to docs/docs/onboarding/access-requests.md index 5b9eb0ff98..49eb04b529 100644 --- a/docs/docs/onboarding/account-access.md +++ b/docs/docs/onboarding/access-requests.md @@ -1,11 +1,11 @@ --- layout: default -title: Account Access +title: Access Requests parent: Onboarding nav_order: 2 --- -# Account Access +# Access Requests {: .no_toc } You'll need access to a few systems to be a fully privileged developer. This section will guide you in making the necessary access requests. @@ -46,6 +46,37 @@ So, to have full AWS access, you will need two things: access to the Cloud VPN Please [follow this how-to guide](https://qmacbis.atlassian.net/l/cp/yY5s5is2) to obtain access to AWS and the Cloud VPN. +## Zscaler VPN + +This project communicates with Seatool. Sometimes, during development, access to the Seatool frontend is helpful. To gain access to it, you must first get access to the Zscaler VPN. + +Please follow these [instructions to gain access to Zscaler](https://qmacbis.atlassian.net/wiki/spaces/DE/pages/3180560407/How+to+get+access+to+CMS+Zscaler+VPN+Access). + +## Snyk + +Snyk is a software tool that specializes in identifying and resolving security vulnerabilities in code dependencies and performing static code analysis. It scans project dependencies for known security issues and provides recommendations for fixing them, enhancing code security. + +CMS has a Snyk installation that our project ties into. It can be found [https://snyk.cms.gov/](here). Access is governed by a CMS job code, as well as Snyk permissions. + +Please follow these steps to gain access to Snyk: +1. Request / Confirm EUA job code: ENT_APPSEC_TOOLS: Access to Enterprise Application Security Tools: Snyk +2. Login to Enterprise User Administration (EUA) +3. Select View My Identity +4. Select the "Job Codes" tab +5. Review your current Job Codes. Do you have ENT_APPSEC_TOOLS? +6. If no, request job code ENT_APPSEC_TOOLS +- On the “Task” sidebar select “Modify My Job Codes +- Be sure to select the “*Confirmation (Required) check box +- Select Next +- At the bottom left of the page select “Add a Job Code” +- Enter the job code you want to add, In this case ENT_APPSEC_TOOLS and select “Search” +- Check the select box and click the “Select” bottom. +- Click “Next” +- Enter a “Justification Reason” and select “Finish” +- Example: I am a CMS contractor, requesting Access to Enterprise Application Security Tools: Snyk order to support development and maintenance of the suite of MACPRO systems supported by Fearless and it’s sub-contractors under Primary Contract Number: GS-35F-115GA:75FCMC22F0093: +- Now wait for the request to be approved. You need ENT_APPSEC_TOOLS before the cloud team will setup your access. +7. Let me know when each user has confirmed that you have the ENT_APPSEC_TOOLS the Job Code and I will send a Snyk invite the email address associated with their EUA ID. + ## Code Climate We use [Code Climate](https://codeclimate.com/) to monitor project quality. This includes running maintainability checks for Pull Requests, which flags code that doesn't meet best practices. Checks include function length, file length, cognitive complexity, and duplication. diff --git a/docs/docs/services/api.md b/docs/docs/services/api.md index 5e78460668..bd03eb14fa 100644 --- a/docs/docs/services/api.md +++ b/docs/docs/services/api.md @@ -20,7 +20,8 @@ There are four endpoints on the api. Each is guarded by AWS IAM, meaning that w - /search (POST): This endpoint accepts search queries from clients in the form of OpenSearch Query DSL queries. Once the query is received, the lambda adds extra query filters to ensure fine grain auth. This works by looking up the user making the call in Cognito, determining what type of user (cms or state) is making the call, determining what states that user has access to (if appropriate), and modifying the query in a way that will only return results for those states. By design, the only thing the search endpoint adds is related to authentication; the rest of the query building is left to the frontend for faster and more flexible development. - /item (POST): The item endpoint is used to fetch details for exactly one record. While you can form a query to do this and use the search endpoint, the item endpoint is for convenience. Simply make a post call containing the ID of the desired record to the item endpoint, and the record will be returned. Note that fine grain auth is still enforced in an identical way to search, whereby you will only obtain results for that ID if you should have access to that ID. - /getAttachmentUrl (POST): This endpoint is used to generate a presigned url for direct client downloading of S3 data, enforcing fine grain auth along the way. This is how we securely allow download of submission attachment data. From the details page, a user may click a file to download. Once clicked, their client makes a post to /getAttachmentUrl with the attachment metadata. The lambda function determines if the caller should or should not have access based on identical logic as the other endpoints (the UI would not display something they cannot download, but this guards against bad actors). If access is allowed, the lambda function generates a presigned url good for 60 seconds and returns it to the client browser, at which point files are downloaded automatically. -- /forms (GET): This endpoint function serves as the backend for handling forms and their associated data. This function provides various features, including retrieving form data, validating access, and serving the requested form content. The request to this endpoint must include a formId in the request body. Optionally, you can include a formVersion parameter. If you access this endpoint with formId without specifying formVersion, it will return the latest version. Form schemas are stored in a Lambda layer. Each form is organized in its directory, and each version is stored within that directory. The Lambda layer is located in the "opt" directory when deployed to aws. To access a specific version of a form with a formId, use the following URL structure: /opt/${formId}/v${formVersion}.json. The JSON form schemas are versioned and stored in Git under the "api/layers" directory. +- /allForms (GET): This endpoint serves GET requests and will return a list off all available webforms and their associated version. the result will look like: { ABP1: [ '1', '2' ], ABP2: [ '1' ] } +- /forms (GET): This endpoint function serves as the backend for handling forms and their associated data. This function provides various features, including retrieving form data, validating access, and serving the requested form content. The request to this endpoint must include a formId in the request body. Optionally, you can include a formVersion parameter. If you access this endpoint with formId without specifying formVersion, it will return the latest version. Form schemas are stored in a Lambda layer. Each form is organized in its directory, and each version is stored within that directory. The Lambda layer is located in the "opt" directory when deployed to aws. To access a specific version of a form with a formId, use the following URL structure: /opt/${formId}/v${formVersion}.js. The form schemas are versioned and stored in Git under the "api/layers" directory. All endpoints and backing functions interact with the OpenSearch data layer. As such, and because OpenSearch is deployed within a VPC, all lambda functions of the api service are VPC based. The functions share a security group that allows outbound traffic. diff --git a/docs/docs/services/email.md b/docs/docs/services/email.md new file mode 100644 index 0000000000..738f5e07bd --- /dev/null +++ b/docs/docs/services/email.md @@ -0,0 +1,14 @@ +--- +layout: default +title: email +parent: Services +--- + +# data +{: .no_toc } + +## Summary +The email service deploys the lambdas, SNS topics, and Configuration Sets needed to send email. + +## Detail +AWS SES is an account-wide service for basic sending and receiving of email. By creating lambdas to build the emails and sending the email with a branch-specific configuration set, we can follow the events of email sending and take action based on those events. \ No newline at end of file diff --git a/docs/docs/team-norms/jira-workflow.md b/docs/docs/team-norms/jira-workflow.md new file mode 100644 index 0000000000..1afd24acb6 --- /dev/null +++ b/docs/docs/team-norms/jira-workflow.md @@ -0,0 +1,77 @@ +--- +layout: default +title: JIRA Workflow +parent: Team Norms +nav_order: 2 +--- + +# JIRA Workflow + +## Introduction + +This document outlines our team's process for managing tasks using the JIRA Kanban board. It's designed to provide clarity and consistency on how we track and progress through work items. + +## Kanban Columns + +### Backlog + +- **Population**: The backlog is populated with new tasks during the planning phase. +- **Prioritization**: Tasks are prioritized based on urgency and importance. + +### Ready + +- **Preparation**: Tasks are moved to "Ready" once they are clearly defined and ready for development. +- **Final Checks**: Ensure that all necessary information and acceptance criteria are present. Story points should be added prior to moving to Ready. + +### In Progress + +- **Active Development**: When a task is actively being worked on, it is moved to "In Progress". +- **Daily Updates**: Developers should provide daily updates or comments on the task to indicate progress. + +### In Review + +- **Code Review**: Tasks are moved here when the development is complete and they are awaiting code review. +- **Peer Feedback**: Team members provide feedback and approval. If feedback requires a significant modification or rewrite to the code, the ticket should be moved back to In Progress. + +### In Testing + +- **Quality Assurance**: Tasks in this column are undergoing thorough testing by the QA team. +- **Env URL**: When a developer moves a ticket into In Testing, the dev should make a comment on the ticket that includes the deployed environment's URL, as well as probably tagging the QA team for convenience. +- **Hands Off**: Developers should not push code that updates environments for work that is In Testing, without coordinating with the QA team. This is to prevent deployments interfering with the QA process. +- **Bug Reporting**: Any issues discovered during testing are reported and linked to the task. +- **Failures**: If a ticket fails QA for reasons that should not be addressed separately (like a bug), the QA team will move the ticket back to In Review. + +### Ready for Merge + +- **Final Approval**: Once testing is complete and the task has passed all checks, it moves to "Ready for Merge". +- **Merge Preparation**: The task is prepared for merging into the main codebase. + +### In Pipeline + +- **Deployment**: Tasks here have been merged to master, and are in the process of being verified on master by the QA team. +- **Monitoring**: Close monitoring of the feature in the live environment for any immediate issues. + +### Done + +- **Completion**: Tasks are moved to "Done" when they are merged to master and verified on master, if applicable. +- **Review**: The team may review completed tasks to ensure all objectives were met. +- **Demo Coordination**: If the completed work is going to be demoed, coordinate a time with Product to relay the context of the feature and how to demonstrate it. + +## Sprint Tracking + +### Current and Upcoming Sprints + +- **Active Sprint**: Tasks currently being worked on are tracked under the active sprint, e.g., "Sprint 2.4". +- **Future Sprints**: Upcoming work items are assigned to future sprints, and periodically rescheduled during refinement as events unfold. + +### Sprint Review + +- **Regular Reviews**: At the end of each sprint, the team reviews progress and adjusts future plans accordingly. +- **Continuous Improvement**: Lessons learned are discussed and processes are adjusted to improve future sprints. + +## Responsibility and Accountability + +- **Ownership**: Team members take ownership of tasks they are working on and update their status accordingly. +- **Collaboration**: The entire team collaborates to ensure tasks move smoothly through the workflow. + +By adhering to this JIRA workflow, we aim to maintain a high level of organization and efficiency within our development process. \ No newline at end of file diff --git a/docs/docs/team-norms/pull-requests.md b/docs/docs/team-norms/pull-requests.md new file mode 100644 index 0000000000..4a4d5a3884 --- /dev/null +++ b/docs/docs/team-norms/pull-requests.md @@ -0,0 +1,65 @@ +--- +layout: default +title: Pull Requests +parent: Team Norms +nav_order: 1 +--- + +# Pull Requests + +## Introduction + +Pull Requests (PRs) are an essential part of our software development process, enabling code review, discussion, and integration. This document outlines our team norms regarding PRs to ensure a smooth, efficient, and collaborative workflow. + +Above all else, please remember: your discretion and judgement supersedes this document. If you're creating or reviewing a Pull Request that needs to break any of these norms for good reason, that's OK. This document is the rule; exceptions are expected. + +## Creating Pull Requests + +### Content and Format + +- **Title**: The title of your PR should be carefully considered. It should be descriptive enough to convey the essence of the changes but concise enough to be easily readable. Think of the title as a brief summary that helps others quickly understand the purpose of the PR. +- **Follow the Template**: We use a Pull Request template to standardize and streamline our PR descriptions. It is expected that all PRs will adhere to this template. + +### Small, Focused Changes + +- **Scope**: Keep PRs focused on a single task or issue to simplify review and discussion. While most pull requests will each address a single ticket, it's OK to handle multiple Jira tickets in one PR when it makes sense. +- **Size**: Aim for small, manageable PRs. Large PRs should be broken down into smaller parts if possible. +- **Formatters**: Be conscious of formatting updates that your IDE may make automatically, or that you may make along the way. Sometimes small, non-functional code changes can clutter a pull request. + +## Reviewing Pull Requests + +### Timeliness + +- **Prompt Reviews**: Team members are expected to review PRs in a timely manner, typically next day or sooner. + +### Constructive Feedback + +- **Respectful Communication**: Provide constructive, respectful feedback. +- **Specific Comments**: Use specific comments to point out issues, suggest improvements, or ask clarifying questions. + +### Testing + +- **Test as Appropriate**: The burden of full end to end testing lies with the author, our automated testing frameworks, and any manual QA process. However, reviewers should test the environment when they think it necessary, perhaps when they've thought of an edge case that might not be covered. +- **Explain your testing**: The PR approach should typically include some detail around how manual tests were performed. This helps greatly in allowing reviewers to sign off without needing to test individually. + +## Merging Pull Requests + +### Passing Checks + +- **CI/CD**: Ensure all continuous integration and deployment checks have passed. +- **Code Quality**: Code should meet our standards for quality and maintainability. + +### Approval + +- **Minimum Approvals**: PRs require a minimum number of approvals (1) from the team members before merging. +- **Outstanding Comments**: If there are comments that ask for action or consideration to be made by the author, please address them prior to merge, regardless if you have approval. + +### Merging +- **Author Merges**: After receiving necessary approvals, the PR author is responsible for merging the code. +- **Squashing Protocol**: When merging into the master branch, always squash and merge. When merging into val and production, create a merge commit. +- **Commit Messages**: We use semantic release to automatically release our product. Semantic Release looks for commit messages with special [commit message syntax](https://semantic-release.gitbook.io/semantic-release/#commit-message-format). Please follow this syntax when crafting your commit message. Note: GitHub will use your PR title as the default commit message when squashing; so, it's recommended to set your PR title equal to the semantic release commit message appropriate for your changeset. + +## Responsibility and Accountability + +- **Ownership**: The author is responsible for addressing feedback and ensuring the PR is ready for merging. +- **Collaboration**: All team members share the responsibility for maintaining a high standard of code through thoughtful PR reviews. diff --git a/docs/docs/team-norms/team-norms.md b/docs/docs/team-norms/team-norms.md new file mode 100644 index 0000000000..d330c81557 --- /dev/null +++ b/docs/docs/team-norms/team-norms.md @@ -0,0 +1,15 @@ +--- +layout: default +title: Team Norms +nav_order: 3 +has_children: true +permalink: docs/team-norms +--- + +# Team Norms +{: .no_toc } + +What's expected and what you can expect +{: .fs-6 .fw-300 } + +wip diff --git a/docs/docs/webforms.md b/docs/docs/webforms.md new file mode 100644 index 0000000000..96012b2bcd --- /dev/null +++ b/docs/docs/webforms.md @@ -0,0 +1,34 @@ +--- +layout: default +title: Webforms +nav_order: 8 +--- + +# Webforms +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +- TOC +{:toc} + +
+### [ClICK HERE]({{ site.url }}{{ site.repo.name }}/metrics/webforms) to view documentation for individual webforms for the {{ site.repo.name }} project. +{: .no_toc } +
+ + +## Purpose of this view +The goal of the webforms view is to be able to view all possible form fields as they relate to the data collected. We try to associate the "name" of the field, which is the key of the data as it is collected and stored, to the prompt and label of the question the user is presented with. + +## What are onemac webforms +These webforms are a replacement for the pdf form uploads previously used in mmdl, etc. These are dynamicly generated forms from a single document that represent the shape of a form and version. There three pieces to this puzzle. +1. The form schema itself. this is the shape of a form that is delivered by the /forms endpoint to the frontend ui. +1. The form genereator. This is a collection of ui react components and `react-hook-form` methods that generate the ui from the form schema and render and validate the form presented to the user. +1. The data. The data collected is added along with its metadata to its parent record. This data can be used to view/approve/edit the form later by admins or other users with appropriate permissions. + +## Things to note +Some of the fields listed in these webforms are FieldGroups and FieldArrays. This means that they are arrays of values that contain groups of like values (think an array of identicaly typed objects). The items within those arrays will have `Parent` values to indicate which parent they belong to. + + diff --git a/serverless-compose.yml b/serverless-compose.yml index 4e8c972e6e..b5f19705ac 100644 --- a/serverless-compose.yml +++ b/serverless-compose.yml @@ -9,6 +9,8 @@ services: path: src/services/data params: ECSFailureTopicArn: ${alerts.ECSFailureTopicArn} + email: + path: src/services/email uploads: path: src/services/uploads ui-infra: diff --git a/src/libs/opensearch-lib.ts b/src/libs/opensearch-lib.ts index 34a3e03005..6d194adf9d 100644 --- a/src/libs/opensearch-lib.ts +++ b/src/libs/opensearch-lib.ts @@ -7,7 +7,7 @@ import * as aws4 from "aws4"; import axios from "axios"; import { aws4Interceptor } from "aws4-axios"; import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; -import { OsIndex } from "shared-types"; +import { opensearch } from "shared-types"; let client: Client; export async function getClient(host: string) { @@ -40,7 +40,7 @@ export async function updateData(host: string, indexObject: any) { export async function bulkUpdateData( host: string, - index: OsIndex, + index: opensearch.Index, arrayOfDocuments: any ) { client = client || (await getClient(host)); @@ -60,7 +60,7 @@ export async function bulkUpdateData( console.log(response); } -export async function deleteIndex(host: string, index: OsIndex) { +export async function deleteIndex(host: string, index: opensearch.Index) { client = client || (await getClient(host)); var response = await client.indices.delete({ index }); } @@ -111,7 +111,7 @@ export async function mapRole( } } -export async function search(host: string, index: OsIndex, query: any) { +export async function search(host: string, index: opensearch.Index, query: any) { client = client || (await getClient(host)); try { const response = await client.search({ @@ -124,7 +124,7 @@ export async function search(host: string, index: OsIndex, query: any) { } } -export async function getItem(host: string, index: OsIndex, id: string) { +export async function getItem(host: string, index: opensearch.Index, id: string) { client = client || (await getClient(host)); try { const response = await client.get({ id, index }); @@ -135,7 +135,7 @@ export async function getItem(host: string, index: OsIndex, id: string) { } // check it exists - then create -export async function createIndex(host: string, index: OsIndex) { +export async function createIndex(host: string, index: opensearch.Index) { client = client || (await getClient(host)); try { const exists = await client.indices.exists({ index }); @@ -150,7 +150,7 @@ export async function createIndex(host: string, index: OsIndex) { export async function updateFieldMapping( host: string, - index: OsIndex, + index: opensearch.Index, properties: object ) { client = client || (await getClient(host)); diff --git a/src/packages/shared-types/action-types/index.ts b/src/packages/shared-types/action-types/index.ts index a5c9f7d4ee..d675be01c1 100644 --- a/src/packages/shared-types/action-types/index.ts +++ b/src/packages/shared-types/action-types/index.ts @@ -3,3 +3,6 @@ export * from "./issue-rai"; export * from "./respond-to-rai"; export * from "./withdraw-rai"; export * from "./withdraw-package"; +export * from "./new-submission"; +export * from "./legacy-submission"; +export * from "./seatool"; diff --git a/src/packages/shared-types/action-types/issue-rai.ts b/src/packages/shared-types/action-types/issue-rai.ts index 04a43251ce..8ec3f2001c 100644 --- a/src/packages/shared-types/action-types/issue-rai.ts +++ b/src/packages/shared-types/action-types/issue-rai.ts @@ -1,34 +1,14 @@ import { z } from "zod"; -import { onemacAttachmentSchema, handleAttachment } from "../attachments"; +import { attachmentSchema } from "../attachments"; export const raiIssueSchema = z.object({ id: z.string(), authority: z.string(), origin: z.string(), requestedDate: z.number(), - attachments: z.array(onemacAttachmentSchema).nullish(), + attachments: z.array(attachmentSchema).nullish(), additionalInformation: z.string().nullable().default(null), submitterName: z.string(), submitterEmail: z.string(), }); export type RaiIssue = z.infer; - -export const transformRaiIssue = (id: string) => { - return raiIssueSchema.transform((data) => ({ - id, - rais: { - [data.requestedDate]: { - request: { - attachments: - data.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - additionalInformation: data.additionalInformation, - submitterName: data.submitterName, - submitterEmail: data.submitterEmail, - }, - }, - }, - })); -}; -export type RaiIssueTransform = z.infer>; diff --git a/src/packages/shared-types/action-types/legacy-submission.ts b/src/packages/shared-types/action-types/legacy-submission.ts new file mode 100644 index 0000000000..8a34833041 --- /dev/null +++ b/src/packages/shared-types/action-types/legacy-submission.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; +import { legacyAttachmentSchema } from "../attachments"; + +// Event schema for legacy records +export const onemacLegacySchema = z.object({ + additionalInformation: z.string().nullable().default(null), + submitterName: z.string(), + submitterEmail: z.string(), + attachments: z.array(legacyAttachmentSchema).nullish(), + raiWithdrawEnabled: z.boolean().default(false), +}); +export type OnemacLegacy = z.infer; diff --git a/src/packages/shared-types/action-types/new-submission.ts b/src/packages/shared-types/action-types/new-submission.ts new file mode 100644 index 0000000000..bbd5afd2e1 --- /dev/null +++ b/src/packages/shared-types/action-types/new-submission.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { attachmentSchema } from "../attachments"; + +// This is the event schema for ne submissions from our system +export const onemacSchema = z.object({ + authority: z.string(), + origin: z.string(), + additionalInformation: z.string().nullable().default(null), + submitterName: z.string(), + submitterEmail: z.string(), + attachments: z.array(attachmentSchema).nullish(), + raiWithdrawEnabled: z.boolean().default(false), +}); + +export type OneMac = z.infer; diff --git a/src/packages/shared-types/action-types/respond-to-rai.ts b/src/packages/shared-types/action-types/respond-to-rai.ts index 8c53a672a8..d66a7be67d 100644 --- a/src/packages/shared-types/action-types/respond-to-rai.ts +++ b/src/packages/shared-types/action-types/respond-to-rai.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { onemacAttachmentSchema, handleAttachment } from "../attachments"; +import { attachmentSchema } from "../attachments"; export const raiResponseSchema = z.object({ id: z.string(), @@ -7,31 +7,9 @@ export const raiResponseSchema = z.object({ origin: z.string(), requestedDate: z.number(), responseDate: z.number(), - attachments: z.array(onemacAttachmentSchema).nullish(), + attachments: z.array(attachmentSchema).nullish(), additionalInformation: z.string().nullable().default(null), submitterName: z.string(), submitterEmail: z.string(), }); export type RaiResponse = z.infer; - -export const transformRaiResponse = (id: string) => { - return raiResponseSchema.transform((data) => ({ - id, - rais: { - [data.requestedDate]: { - response: { - attachments: - data.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - additionalInformation: data.additionalInformation, - submitterName: data.submitterName, - submitterEmail: data.submitterEmail, - }, - }, - }, - })); -}; -export type RaiResponseTransform = z.infer< - ReturnType ->; diff --git a/src/packages/shared-types/action-types/seatool.ts b/src/packages/shared-types/action-types/seatool.ts new file mode 100644 index 0000000000..808a45f99b --- /dev/null +++ b/src/packages/shared-types/action-types/seatool.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; + +// Reusable schema for officer data. +export const seatoolOfficerSchema = z.object({ + OFFICER_ID: z.number(), + FIRST_NAME: z.string(), + LAST_NAME: z.string(), +}); +export type SeatoolOfficer = z.infer; + +export const seatoolSchema = z.object({ + ACTION_OFFICERS: z.array(seatoolOfficerSchema).nullish(), + LEAD_ANALYST: z.array(seatoolOfficerSchema).nullable(), + PLAN_TYPES: z + .array( + z.object({ + PLAN_TYPE_NAME: z.string(), + }) + ) + .nonempty() + .nullable(), + STATE_PLAN: z.object({ + SUBMISSION_DATE: z.number().nullable(), + PLAN_TYPE: z.number().nullable(), + LEAD_ANALYST_ID: z.number().nullable(), + CHANGED_DATE: z.number().nullable(), + APPROVED_EFFECTIVE_DATE: z.number().nullable(), + PROPOSED_DATE: z.number().nullable(), + SPW_STATUS_ID: z.number().nullable(), + STATE_CODE: z.string().nullish(), + STATUS_DATE: z.number().nullish(), + SUMMARY_MEMO: z.string().nullish(), + TITLE_NAME: z.string().nullish(), + }), + SPW_STATUS: z + .array( + z.object({ + SPW_STATUS_DESC: z.string().nullable(), + SPW_STATUS_ID: z.number().nullable(), + }) + ) + .nullable(), + RAI: z + .array( + z.object({ + RAI_RECEIVED_DATE: z.number().nullable(), + RAI_REQUESTED_DATE: z.number().nullable(), + RAI_WITHDRAWN_DATE: z.number().nullable(), + }) + ) + .nullable(), + ACTIONTYPES: z + .array( + z.object({ + ACTION_ID: z.number(), + ACTION_NAME: z.string(), + PLAN_TYPE_ID: z.number(), + }) + ) + .nullable(), +}); +export type SeaTool = z.infer; diff --git a/src/packages/shared-types/action-types/toggle-withdraw-rai-enabled.ts b/src/packages/shared-types/action-types/toggle-withdraw-rai-enabled.ts index 948d782c15..16abffc90c 100644 --- a/src/packages/shared-types/action-types/toggle-withdraw-rai-enabled.ts +++ b/src/packages/shared-types/action-types/toggle-withdraw-rai-enabled.ts @@ -11,13 +11,3 @@ export const toggleWithdrawRaiEnabledSchema = z.object({ export type ToggleWithdrawRaiEnabled = z.infer< typeof toggleWithdrawRaiEnabledSchema >; - -export const transformToggleWithdrawRaiEnabled = (id: string) => { - return toggleWithdrawRaiEnabledSchema.transform((data) => ({ - id, - raiWithdrawEnabled: data.raiWithdrawEnabled, - })); -}; -export type ToggleWithdrawRaiEnabledTransform = z.infer< - ReturnType ->; diff --git a/src/packages/shared-types/action-types/withdraw-package.ts b/src/packages/shared-types/action-types/withdraw-package.ts index da1713c2c0..1a25a114a9 100644 --- a/src/packages/shared-types/action-types/withdraw-package.ts +++ b/src/packages/shared-types/action-types/withdraw-package.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { onemacAttachmentSchema } from "../attachments"; +import { attachmentSchema } from "../attachments"; // Temporary, will be refactored to an extendable schema with Brian/Mike's back-end // work. @@ -11,20 +11,9 @@ export const withdrawPackageSchema = z.object({ .string() .max(4000, "This field may only be up to 4000 characters.") .optional(), - attachments: z.array(onemacAttachmentSchema), + attachments: z.array(attachmentSchema), submitterName: z.string(), submitterEmail: z.string(), }); export type WithdrawPackage = z.infer; - -export const transformWithdrawPackage = (id: string) => { - // This does nothing. Just putting the mechanics in place. - return withdrawPackageSchema.transform((data) => ({ - id, - raiWithdrawEnabled: false, - })); -}; -export type WithdrawPackageTransform = z.infer< - ReturnType ->; diff --git a/src/packages/shared-types/action-types/withdraw-rai.ts b/src/packages/shared-types/action-types/withdraw-rai.ts index c6cd546ffa..c77477a817 100644 --- a/src/packages/shared-types/action-types/withdraw-rai.ts +++ b/src/packages/shared-types/action-types/withdraw-rai.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { onemacAttachmentSchema, handleAttachment } from "../attachments"; +import { attachmentSchema } from "../attachments"; export const raiWithdrawSchema = z.object({ id: z.string(), @@ -7,32 +7,9 @@ export const raiWithdrawSchema = z.object({ origin: z.string(), requestedDate: z.number(), withdrawnDate: z.number(), - attachments: z.array(onemacAttachmentSchema).nullish(), + attachments: z.array(attachmentSchema).nullish(), additionalInformation: z.string().nullable().default(null), submitterName: z.string(), submitterEmail: z.string(), }); export type RaiWithdraw = z.infer; - -export const transformRaiWithdraw = (id: string) => { - return raiWithdrawSchema.transform((data) => ({ - id, - raiWithdrawEnabled: false, - rais: { - [data.requestedDate]: { - withdraw: { - attachments: - data.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - additionalInformation: data.additionalInformation, - submitterName: data.submitterName, - submitterEmail: data.submitterEmail, - }, - }, - }, - })); -}; -export type RaiWithdrawTransform = z.infer< - ReturnType ->; diff --git a/src/packages/shared-types/attachments.ts b/src/packages/shared-types/attachments.ts index 18ef2c06f9..0e1a73eece 100644 --- a/src/packages/shared-types/attachments.ts +++ b/src/packages/shared-types/attachments.ts @@ -26,44 +26,38 @@ export const attachmentTitleMap: Record = { export type AttachmentKey = keyof typeof attachmentTitleMap; export type AttachmentTitle = typeof attachmentTitleMap[AttachmentKey]; -export const onemacAttachmentSchema = z.object({ - s3Key: z.string().nullish(), +export const attachmentSchema = z.object({ filename: z.string(), title: z.string(), - contentType: z.string().nullish(), - url: z.string().url().nullish(), - bucket: z.string().nullish(), - key: z.string().nullish(), - uploadDate: z.number().nullish(), + bucket: z.string(), + key: z.string(), + uploadDate: z.number(), }); -export type OnemacAttachmentSchema = z.infer; +export type Attachment = z.infer; -export function handleAttachment(attachment: OnemacAttachmentSchema) { - let bucket = ""; - let key = ""; - let uploadDate = 0; - if ("bucket" in attachment) { - bucket = attachment.bucket as string; - } - if ("key" in attachment) { - key = attachment.key as string; - } - if ("uploadDate" in attachment) { - uploadDate = attachment.uploadDate as number; - } - if (bucket == "") { - const parsedUrl = s3ParseUrl(attachment.url || ""); - if (!parsedUrl) return null; - bucket = parsedUrl.bucket; - key = parsedUrl.key; - uploadDate = parseInt(attachment.s3Key?.split("/")[0] || "0"); - } +// Attachment schema for legacy records +export const legacyAttachmentSchema = z.object({ + s3Key: z.string(), + filename: z.string(), + title: z.string(), + contentType: z.string(), + url: z.string().url(), +}); +export type LegacyAttachment = z.infer; +export function handleLegacyAttachment( + attachment: LegacyAttachment +): Attachment | null { + const parsedUrl = s3ParseUrl(attachment.url || ""); + if (!parsedUrl) return null; + const bucket = parsedUrl.bucket; + const key = parsedUrl.key; + const uploadDate = parseInt(attachment.s3Key?.split("/")[0] || "0"); return { title: attachment.title, filename: attachment.filename, uploadDate, bucket, key, - }; + } as Attachment; } diff --git a/src/packages/shared-types/forms.ts b/src/packages/shared-types/forms.ts index 5701f69cf5..8d28ddf1ea 100644 --- a/src/packages/shared-types/forms.ts +++ b/src/packages/shared-types/forms.ts @@ -22,8 +22,11 @@ export type RHFSlotProps = { name: string; label?: string; labelStyling?: string; + formItemStyling?: string; groupNamePrefix?: string; description?: string; + descriptionAbove?: boolean; + descriptionStyling?: string; dependency?: DependencyRule; rules?: RegisterOptions; } & { @@ -41,6 +44,7 @@ export type RHFSlotProps = { export type RHFOption = { label: string; value: string; + dependency?: DependencyRule; form?: FormGroup[]; slots?: RHFSlotProps[]; }; @@ -138,4 +142,7 @@ export interface DependencyRule { export interface DependencyWrapperProps { name?: string; dependency?: DependencyRule; + // The dependency wraper is passed two props from the checkbox button RHF used to change its value for depedency show/hide logic + changeMethod?: (...event: any[]) => void; + parentValue?: string[]; } diff --git a/src/packages/shared-types/guides.ts b/src/packages/shared-types/guides.ts new file mode 100644 index 0000000000..22f4ee1f9c --- /dev/null +++ b/src/packages/shared-types/guides.ts @@ -0,0 +1,6 @@ +export type Guide = { + title: string; + linkTitle: string; + href: string; + targetBlank?: boolean; +}; diff --git a/src/packages/shared-types/index.ts b/src/packages/shared-types/index.ts index df749505fd..6cfdfbab33 100644 --- a/src/packages/shared-types/index.ts +++ b/src/packages/shared-types/index.ts @@ -1,10 +1,7 @@ export * from "./issue"; export * from "./user"; export * from "./errors"; -export * from "./seatool"; -export * from "./onemac"; -export * from "./onemacLegacy"; -export * from "./opensearch"; +export * as opensearch from "./opensearch"; export * from "./uploads"; export * from "./actions"; export * from "./attachments"; @@ -14,3 +11,5 @@ export * from "./forms"; export * from "./inputs"; export * from "./states"; export * from "./statusHelper"; +export * from "./guides"; +export * from "./lambda-events"; diff --git a/src/packages/shared-types/lambda-events.ts b/src/packages/shared-types/lambda-events.ts new file mode 100644 index 0000000000..189c135afe --- /dev/null +++ b/src/packages/shared-types/lambda-events.ts @@ -0,0 +1,22 @@ +export type KafkaEvent = { + /** + * @example "SelfManagedKafka" + */ + eventSource: string; + /** + * @example: "b-1.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094,b-2.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094,b-3.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094" + */ + bootstrapServers: string; // comma separated string + records: Record; +}; + +export type KafkaRecord = { + topic: string; + partition: number; + offset: number; + timestamp: number; + timestampType: string; + key: string; + headers: Record; + value: string; // Kafka records typically have values as base64-encoded strings +}; diff --git a/src/packages/shared-types/onemac.ts b/src/packages/shared-types/onemac.ts deleted file mode 100644 index 5c4a3b7608..0000000000 --- a/src/packages/shared-types/onemac.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { z } from "zod"; -import { onemacAttachmentSchema, handleAttachment } from "./attachments"; - -// This is the event schema for ne submissions from our system -export const onemacSchema = z.object({ - authority: z.string(), - origin: z.string(), - additionalInformation: z.string().nullable().default(null), - submitterName: z.string(), - submitterEmail: z.string(), - attachments: z.array(onemacAttachmentSchema).nullish(), - raiWithdrawEnabled: z.boolean().default(false), - raiResponses: z - .array( - z.object({ - additionalInformation: z.string().nullable().default(null), - submissionTimestamp: z.number(), - attachments: z.array(onemacAttachmentSchema).nullish(), - }) - ) - .nullish(), -}); - -export type OneMac = z.infer; - -export const transformOnemac = (id: string) => { - return onemacSchema.transform((data) => { - const transformedData = { - id, - attachments: - data.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - raiWithdrawEnabled: data.raiWithdrawEnabled, - additionalInformation: data.additionalInformation, - submitterEmail: data.submitterEmail, - submitterName: data.submitterName === "-- --" ? null : data.submitterName, - origin: "OneMAC", - rais: {} as { - [key: number]: { - requestedDate?: string; - responseDate?: string; - withdrawnDate?: string; - response?: { - additionalInformation: string; - submitterName: string | null; - submitterEmail: string | null; - attachments: any[] | null; // You might want to specify the type of attachments - }; - request?: { - additionalInformation: string; - submitterName: string | null; - submitterEmail: string | null; - attachments: any[] | null; // You might want to specify the type of attachments - }; - }; - }, - }; - if (data.raiResponses) { - data.raiResponses.forEach((raiResponse, index) => { - // We create an rai keyed off the index, because we don't know which rai it was in response to. Best we can do. - transformedData["rais"][index] = { - responseDate: raiResponse.submissionTimestamp.toString(), - response: { - additionalInformation: raiResponse.additionalInformation || "", - submitterName: null, - submitterEmail: null, - attachments: - raiResponse.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - }, - }; - }); - } - return transformedData; - }); -}; -export type OnemacTransform = z.infer>; - -export type OnemacRecordsToDelete = Omit< - { - [Property in keyof OnemacTransform]: null; - }, - "id" | "rais" -> & { id: string }; diff --git a/src/packages/shared-types/onemacLegacy.ts b/src/packages/shared-types/onemacLegacy.ts deleted file mode 100644 index 762f21c69c..0000000000 --- a/src/packages/shared-types/onemacLegacy.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { z } from "zod"; -import { onemacAttachmentSchema, handleAttachment } from "./attachments"; - -// This is the event schema we can expect streaming from legacy onemac. -// It should be used by the sink to safe parse and then transform before publishing to opensearch. -export const onemacLegacySchema = z.object({ - additionalInformation: z.string().nullable().default(null), - submitterName: z.string(), - submitterEmail: z.string(), - attachments: z.array(onemacAttachmentSchema).nullish(), - raiWithdrawEnabled: z.boolean().default(false), - raiResponses: z - .array( - z.object({ - additionalInformation: z.string().nullable().default(null), - submissionTimestamp: z.number(), - attachments: z.array(onemacAttachmentSchema).nullish(), - }) - ) - .nullish(), -}); -export type OnemacLegacy = z.infer; - -export const transformOnemacLegacy = (id: string) => { - return onemacLegacySchema.transform((data) => { - const transformedData = { - id, - attachments: - data.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - raiWithdrawEnabled: data.raiWithdrawEnabled, - additionalInformation: data.additionalInformation, - submitterEmail: data.submitterEmail, - submitterName: data.submitterName === "-- --" ? null : data.submitterName, - origin: "OneMAC", - rais: {} as { - [key: number]: { - requestedDate?: string; - responseDate?: string; - withdrawnDate?: string; - response?: { - additionalInformation: string; - submitterName: string | null; - submitterEmail: string | null; - attachments: any[] | null; // You might want to specify the type of attachments - }; - request?: { - additionalInformation: string; - submitterName: string | null; - submitterEmail: string | null; - attachments: any[] | null; // You might want to specify the type of attachments - }; - }; - }, - }; - if (data.raiResponses) { - data.raiResponses.forEach((raiResponse, index) => { - // We create an rai keyed off the index, because we don't know which rai it was in response to. Best we can do. - transformedData["rais"][index] = { - responseDate: raiResponse.submissionTimestamp.toString(), - response: { - additionalInformation: raiResponse.additionalInformation || "", - submitterName: null, - submitterEmail: null, - attachments: - raiResponse.attachments?.map((attachment) => { - return handleAttachment(attachment); - }) ?? null, - }, - }; - }); - } - return transformedData; - }); -}; -export type OnemacLegacyTransform = z.infer< - ReturnType ->; - -export type OnemacLegacyRecordsToDelete = Omit< - { - [Property in keyof OnemacLegacyTransform]: null; - }, - "id" | "rais" -> & { id: string }; diff --git a/src/packages/shared-types/opensearch.ts b/src/packages/shared-types/opensearch.ts deleted file mode 100644 index 8e704c6b53..0000000000 --- a/src/packages/shared-types/opensearch.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - SeaToolTransform, - OnemacTransform, - OnemacLegacyTransform, - RaiIssueTransform, - RaiResponseTransform, - RaiWithdrawTransform, - WithdrawPackageTransform, - ToggleWithdrawRaiEnabledTransform, -} from "./"; - -export type OsHit = { - _index: string; - _id: string; - _score: number; - _source: T; - sort: Array; -}; -export type OsHits = { - hits: OsHit[]; - max_score: number; - total: { value: number; relation: "eq" }; -}; - -export type OsResponse = { - _shards: { - total: number; - failed: number; - successful: number; - skipped: number; - }; - hits: OsHits; - total: { - value: number; - }; - max_score: number | null; - took: number; - timed_out: boolean; - aggregations?: OsAggResult; -}; - -export type OsMainSourceItem = OnemacTransform & - OnemacLegacyTransform & - SeaToolTransform & - RaiIssueTransform & - RaiResponseTransform & - RaiWithdrawTransform & - WithdrawPackageTransform & - ToggleWithdrawRaiEnabledTransform; -export type OsMainSearchResponse = OsResponse; -export type SearchData = OsHits; -export type ItemResult = OsHit & { - found: boolean; -}; - -export type OsFilterType = - | "term" - | "terms" - | "match" - | "range" - | "search" - | "global_search" - | "exists"; - -export type OsRangeValue = { gte?: string; lte?: string }; -export type OsFilterValue = string | string[] | number | boolean | OsRangeValue; -export type OsField = - | keyof OsMainSourceItem - | `${keyof OsMainSourceItem}.keyword`; - -export type OsFilterable = { - type: OsFilterType; - label?: string; - component?: string; - field: OsField; - value: OsFilterValue; - prefix: "must" | "must_not" | "should" | "filter"; -}; - -export type OsQueryState = { - sort: { field: OsField; order: "asc" | "desc" }; - pagination: { number: number; size: number }; - filters: OsFilterable[]; - search?: string; -}; - -export type OsAggQuery = { - name: string; - type: OsFilterType; - field: OsField; - size: number; -}; - -export type OsAggBucket = { key: string; doc_count: number }; - -export type OsAggResult = Record< - string, - { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: OsAggBucket[]; - } ->; - -export type OsExportHeaderOptions = { - transform: (data: TData) => string; - name: string; -}; - -export type OsIndex = "main" | "seatool" | "changelog"; diff --git a/src/packages/shared-types/opensearch/_.ts b/src/packages/shared-types/opensearch/_.ts new file mode 100644 index 0000000000..045011e32a --- /dev/null +++ b/src/packages/shared-types/opensearch/_.ts @@ -0,0 +1,88 @@ +export type Hit = { + _index: string; + _id: string; + _score: number; + _source: T; + sort: Array; +}; +export type Hits = { + hits: Hit[]; + max_score: number; + total: { value: number; relation: "eq" }; +}; + +export type Response = { + _shards: { + total: number; + failed: number; + successful: number; + skipped: number; + }; + hits: Hits; + total: { + value: number; + }; + max_score: number | null; + took: number; + timed_out: boolean; + aggregations?: AggResult; +}; + +export type FilterType = + | "term" + | "terms" + | "match" + | "range" + | "search" + | "global_search" + | "exists"; + +export type RangeValue = { gte?: string; lte?: string }; +export type FilterValue = + | string + | string[] + | number + | boolean + | RangeValue + | null; + +export type Filterable<_FIELD> = { + type: FilterType; + label?: string; + component?: string; + field: _FIELD; + value: FilterValue; + prefix: "must" | "must_not" | "should" | "filter"; +}; + +export type QueryState<_FIELD> = { + sort: { field: _FIELD; order: "asc" | "desc" }; + pagination: { number: number; size: number }; + filters: Filterable<_FIELD>[]; + search?: string; +}; + +export type AggQuery<_FIELD> = { + name: string; + type: FilterType; + field: _FIELD; + size: number; +}; + +export type AggBucket = { key: string; doc_count: number }; + +export type AggResult = Record< + string, + { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: AggBucket[]; + } +>; + +export type ExportHeaderOptions = { + transform: (data: TData) => string; + name: string; +}; + +export type Index = "main" | "seatool" | "changelog"; diff --git a/src/packages/shared-types/opensearch/changelog/index.ts b/src/packages/shared-types/opensearch/changelog/index.ts new file mode 100644 index 0000000000..6f607cf631 --- /dev/null +++ b/src/packages/shared-types/opensearch/changelog/index.ts @@ -0,0 +1,38 @@ +import { + Response as Res, + Hit, + Filterable as FIL, + QueryState, + AggQuery, + ExportHeaderOptions, +} from "./../_"; +import { + OneMac, + RaiIssue, + RaiResponse, + RaiWithdraw, + WithdrawPackage, + ToggleWithdrawRaiEnabled, +} from "../../action-types"; + +export type Document = OneMac & + WithdrawPackage & + RaiResponse & + RaiIssue & + RaiWithdraw & + ToggleWithdrawRaiEnabled & { + actionType: string; + timestamp: string; + packageId: string; + }; + +export type Response = Res; +export type ItemResult = Hit & { + found: boolean; +}; + +export type Field = keyof Document | `${keyof Document}.keyword`; +export type Filterable = FIL; +export type State = QueryState; +export type Aggs = AggQuery; +export type ExportHeader = ExportHeaderOptions; diff --git a/src/packages/shared-types/opensearch/index.ts b/src/packages/shared-types/opensearch/index.ts new file mode 100644 index 0000000000..36c6bf521b --- /dev/null +++ b/src/packages/shared-types/opensearch/index.ts @@ -0,0 +1,3 @@ +export * as changelog from "./changelog"; +export * as main from "./main"; +export * from "./_"; diff --git a/src/packages/shared-types/opensearch/main/index.ts b/src/packages/shared-types/opensearch/main/index.ts new file mode 100644 index 0000000000..d5de6a25f0 --- /dev/null +++ b/src/packages/shared-types/opensearch/main/index.ts @@ -0,0 +1,40 @@ +import { + Response as Res, + Hit, + Filterable as FIL, + QueryState, + AggQuery, + ExportHeaderOptions, +} from "./../_"; +import { z } from "zod"; +import { ItemResult as Changelog } from "./../changelog"; +import { + newSubmission, + legacySubmission, + withdrawPackage, + withdrawRai, + toggleWithdrawEnabled, + seatool, +} from "./transforms"; + +export type Document = z.infer & + z.infer & + z.infer & + z.infer & + z.infer & + z.infer & { + changelog?: Changelog[]; + }; + +export type Response = Res; +export type ItemResult = Hit & { + found: boolean; +}; + +export type Field = keyof Document | `${keyof Document}.keyword`; +export type Filterable = FIL; +export type State = QueryState; +export type Aggs = AggQuery; +export type ExportHeader = ExportHeaderOptions; + +export * from "./transforms"; diff --git a/src/packages/shared-types/opensearch/main/transforms/index.ts b/src/packages/shared-types/opensearch/main/transforms/index.ts new file mode 100644 index 0000000000..afe62b5065 --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/index.ts @@ -0,0 +1,6 @@ +export * as legacySubmission from "./legacy-submission"; +export * as newSubmission from "./new-submission"; +export * as toggleWithdrawEnabled from "./toggle-withdraw-enabled"; +export * as withdrawPackage from "./withdraw-package"; +export * as withdrawRai from "./withdraw-rai-response"; +export * as seatool from "./seatool"; diff --git a/src/packages/shared-types/opensearch/main/transforms/legacy-submission.ts b/src/packages/shared-types/opensearch/main/transforms/legacy-submission.ts new file mode 100644 index 0000000000..f8a549f58c --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/legacy-submission.ts @@ -0,0 +1,18 @@ +import { onemacLegacySchema, handleLegacyAttachment } from "../../.."; + +export const transform = (id: string) => { + return onemacLegacySchema.transform((data) => { + const transformedData = { + id, + attachments: data.attachments?.map(handleLegacyAttachment) ?? null, + raiWithdrawEnabled: data.raiWithdrawEnabled, + additionalInformation: data.additionalInformation, + submitterEmail: data.submitterEmail, + submitterName: data.submitterName === "-- --" ? null : data.submitterName, + origin: "OneMAC", + }; + return transformedData; + }); +}; + +export type Schema = ReturnType; diff --git a/src/packages/shared-types/opensearch/main/transforms/new-submission.ts b/src/packages/shared-types/opensearch/main/transforms/new-submission.ts new file mode 100644 index 0000000000..c91a5911aa --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/new-submission.ts @@ -0,0 +1,18 @@ +import { onemacSchema } from "../../.."; + +export const transform = (id: string) => { + return onemacSchema.transform((data) => { + const transformedData = { + id, + attachments: data.attachments, + raiWithdrawEnabled: data.raiWithdrawEnabled, + additionalInformation: data.additionalInformation, + submitterEmail: data.submitterEmail, + submitterName: data.submitterName === "-- --" ? null : data.submitterName, + origin: "OneMAC", + }; + return transformedData; + }); +}; + +export type Schema = ReturnType; diff --git a/src/packages/shared-types/seatool.ts b/src/packages/shared-types/opensearch/main/transforms/seatool.ts similarity index 60% rename from src/packages/shared-types/seatool.ts rename to src/packages/shared-types/opensearch/main/transforms/seatool.ts index c7ef4833bb..11b2c6b00f 100644 --- a/src/packages/shared-types/seatool.ts +++ b/src/packages/shared-types/opensearch/main/transforms/seatool.ts @@ -1,6 +1,13 @@ -import { z } from "zod"; -import { SEATOOL_STATUS, getStatus } from "./statusHelper"; -import { PlanType } from "./planType"; +import { + seatoolSchema, + SEATOOL_STATUS, + getStatus, + finalDispositionStatuses, + SeaTool, + SeatoolOfficer, +} from "../../.."; + +import { PlanType } from "../../../planType"; type AuthorityType = "SPA" | "WAIVER" | "MEDICAID" | "CHIP"; @@ -17,7 +24,7 @@ const authorityLookup = (val: number | null): null | string => { return lookup[val]; }; -function getLeadAnalyst(eventData: SeaToolSink) { +function getLeadAnalyst(eventData: SeaTool) { let leadAnalystOfficerId: null | number = null; let leadAnalystName: null | string = null; @@ -41,7 +48,7 @@ function getLeadAnalyst(eventData: SeaToolSink) { }; } -const getRaiDate = (data: SeaToolSink) => { +const getRaiDate = (data: SeaTool) => { let raiReceivedDate: null | string = null; let raiRequestedDate: null | string = null; let raiWithdrawnDate: null | string = null; @@ -76,87 +83,44 @@ const getRaiDate = (data: SeaToolSink) => { }; }; -const zActionOfficer = z.object({ - OFFICER_ID: z.number(), - FIRST_NAME: z.string(), - LAST_NAME: z.string(), -}); -type ActionOfficer = z.infer; - -export const seatoolSchema = z.object({ - ACTION_OFFICERS: z.array(zActionOfficer).nullish(), - LEAD_ANALYST: z.array(zActionOfficer).nullable(), - PLAN_TYPES: z - .array( - z.object({ - PLAN_TYPE_NAME: z.string(), - }) - ) - .nonempty() - .nullable(), - STATE_PLAN: z.object({ - SUBMISSION_DATE: z.number().nullable(), - PLAN_TYPE: z.number().nullable(), - LEAD_ANALYST_ID: z.number().nullable(), - CHANGED_DATE: z.number().nullable(), - APPROVED_EFFECTIVE_DATE: z.number().nullable(), - PROPOSED_DATE: z.number().nullable(), - SPW_STATUS_ID: z.number().nullable(), - STATE_CODE: z.string().nullish(), - STATUS_DATE: z.number().nullish(), - SUMMARY_MEMO: z.string().nullish(), - TITLE_NAME: z.string().nullish(), - }), - SPW_STATUS: z - .array( - z.object({ - SPW_STATUS_DESC: z.string().nullable(), - SPW_STATUS_ID: z.number().nullable(), - }) - ) - .nullable(), - RAI: z - .array( - z.object({ - RAI_RECEIVED_DATE: z.number().nullable(), - RAI_REQUESTED_DATE: z.number().nullable(), - RAI_WITHDRAWN_DATE: z.number().nullable(), - }) - ) - .nullable(), - ACTIONTYPES: z - .array( - z.object({ - ACTION_ID: z.number(), - ACTION_NAME: z.string(), - PLAN_TYPE_ID: z.number(), - }) - ) - .nullable(), -}); - const getDateStringOrNullFromEpoc = (epocDate: number | null | undefined) => epocDate !== null && epocDate !== undefined ? new Date(epocDate).toISOString() : null; const compileSrtList = ( - officers: ActionOfficer[] | null | undefined + officers: SeatoolOfficer[] | null | undefined ): string[] => officers?.length ? officers.map((o) => `${o.FIRST_NAME} ${o.LAST_NAME}`) : []; -const getFinalDispositionDate = (status: string, record: SeaToolSink) => { - const finalDispositionStatuses = [ - SEATOOL_STATUS.APPROVED, - SEATOOL_STATUS.DISAPPROVED, - SEATOOL_STATUS.WITHDRAWN, - ]; +const getFinalDispositionDate = (status: string, record: SeaTool) => { return status && finalDispositionStatuses.includes(status) ? getDateStringOrNullFromEpoc(record.STATE_PLAN.STATUS_DATE) : null; }; -export const transformSeatoolData = (id: string) => { +const isInSecondClock = ( + raiReceivedDate: any, + raiWithdrawnDate: any, + seatoolStatus: any, + authority: any +) => { + if ( + authority != "CHIP" && // if it's not a chip + [ + SEATOOL_STATUS.PENDING, + SEATOOL_STATUS.PENDING_CONCURRENCE, + SEATOOL_STATUS.PENDING_APPROVAL, + ].includes(seatoolStatus) && // if it's in pending + raiReceivedDate && // if its latest rai has a received date + !raiWithdrawnDate // if the latest rai has not been withdrawn + ) { + return true; // then we're in second clock + } + return false; // otherwise, we're not +}; + +export const transform = (id: string) => { return seatoolSchema.transform((data) => { const { leadAnalystName, leadAnalystOfficerId } = getLeadAnalyst(data); const { raiReceivedDate, raiRequestedDate, raiWithdrawnDate } = @@ -166,27 +130,6 @@ export const transformSeatoolData = (id: string) => { (item) => item.SPW_STATUS_ID === data.STATE_PLAN.SPW_STATUS_ID )?.SPW_STATUS_DESC || "Unknown"; const { stateStatus, cmsStatus } = getStatus(seatoolStatus); - const rais: Record< - number, - { - requestedDate: number; - receivedDate: number | null; - withdrawnDate: number | null; - } - > = {}; - if (data.RAI) { - data.RAI.forEach((rai) => { - // Should never be null, but if it is there's nothing we can do with it. - if (rai.RAI_REQUESTED_DATE === null) { - return; - } - rais[rai.RAI_REQUESTED_DATE] = { - requestedDate: rai.RAI_REQUESTED_DATE, - receivedDate: rai.RAI_RECEIVED_DATE, - withdrawnDate: rai.RAI_WITHDRAWN_DATE, - }; - }); - } return { id, actionType: data.ACTIONTYPES?.[0].ACTION_NAME, @@ -199,6 +142,8 @@ export const transformSeatoolData = (id: string) => { description: data.STATE_PLAN.SUMMARY_MEMO, finalDispositionDate: getFinalDispositionDate(seatoolStatus, data), leadAnalystOfficerId, + initialIntakeNeeded: + !leadAnalystName && seatoolStatus !== SEATOOL_STATUS.WITHDRAWN, leadAnalystName, planType: data.PLAN_TYPES?.[0].PLAN_TYPE_NAME as PlanType | null, planTypeId: data.STATE_PLAN.PLAN_TYPE, @@ -206,7 +151,6 @@ export const transformSeatoolData = (id: string) => { raiReceivedDate, raiRequestedDate, raiWithdrawnDate, - rais, reviewTeam: compileSrtList(data.ACTION_OFFICERS), state: data.STATE_PLAN.STATE_CODE, stateStatus: stateStatus || SEATOOL_STATUS.UNKNOWN, @@ -217,15 +161,13 @@ export const transformSeatoolData = (id: string) => { data.STATE_PLAN.SUBMISSION_DATE ), subject: data.STATE_PLAN.TITLE_NAME, + secondClock: isInSecondClock( + raiReceivedDate, + raiWithdrawnDate, + seatoolStatus, + authorityLookup(data.STATE_PLAN.PLAN_TYPE) + ), }; }); }; - -export type SeaToolTransform = z.infer>; -export type SeaToolSink = z.infer; -export type SeaToolRecordsToDelete = Omit< - { - [Property in keyof SeaToolTransform]: null; - }, - "id" | "rais" -> & { id: string }; +export type Schema = ReturnType; diff --git a/src/packages/shared-types/opensearch/main/transforms/toggle-withdraw-enabled.ts b/src/packages/shared-types/opensearch/main/transforms/toggle-withdraw-enabled.ts new file mode 100644 index 0000000000..03538271be --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/toggle-withdraw-enabled.ts @@ -0,0 +1,10 @@ +import { toggleWithdrawRaiEnabledSchema } from "../../.."; + +export const transform = (id: string) => { + return toggleWithdrawRaiEnabledSchema.transform((data) => ({ + id, + raiWithdrawEnabled: data.raiWithdrawEnabled, + })); +}; + +export type Schema = ReturnType; diff --git a/src/packages/shared-types/opensearch/main/transforms/withdraw-package.ts b/src/packages/shared-types/opensearch/main/transforms/withdraw-package.ts new file mode 100644 index 0000000000..ce2a2512ed --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/withdraw-package.ts @@ -0,0 +1,11 @@ +import { withdrawPackageSchema } from "../../.."; + +export const transform = (id: string) => { + // This does nothing. Just putting the mechanics in place. + return withdrawPackageSchema.transform(() => ({ + id, + raiWithdrawEnabled: false, + })); +}; + +export type Schema = ReturnType; diff --git a/src/packages/shared-types/opensearch/main/transforms/withdraw-rai-response.ts b/src/packages/shared-types/opensearch/main/transforms/withdraw-rai-response.ts new file mode 100644 index 0000000000..c65ed6fd1a --- /dev/null +++ b/src/packages/shared-types/opensearch/main/transforms/withdraw-rai-response.ts @@ -0,0 +1,10 @@ +import { raiWithdrawSchema } from "../../.."; + +export const transform = (id: string) => { + return raiWithdrawSchema.transform(() => ({ + id, + raiWithdrawEnabled: false, + })); +}; + +export type Schema = ReturnType; diff --git a/src/packages/shared-types/statusHelper.ts b/src/packages/shared-types/statusHelper.ts index 7357847771..0696565260 100644 --- a/src/packages/shared-types/statusHelper.ts +++ b/src/packages/shared-types/statusHelper.ts @@ -38,6 +38,12 @@ const statusToDisplayToCmsUser = { [SEATOOL_STATUS.PENDING_OFF_THE_CLOCK]: "Pending - Off the Clock", }; +export const finalDispositionStatuses = [ + SEATOOL_STATUS.APPROVED, + SEATOOL_STATUS.DISAPPROVED, + SEATOOL_STATUS.WITHDRAWN, +]; + export const getStatus = (seatoolStatus?: string | null) => { const stateStatus = statusToDisplayToStateUser[seatoolStatus ?? "Unknown"]; const cmsStatus = statusToDisplayToCmsUser[seatoolStatus ?? "Unknown"]; diff --git a/src/packages/shared-types/tests/onemac-legacy.test.ts b/src/packages/shared-types/tests/onemac-legacy.test.ts index ed38b1a60d..3cd59e3a9a 100644 --- a/src/packages/shared-types/tests/onemac-legacy.test.ts +++ b/src/packages/shared-types/tests/onemac-legacy.test.ts @@ -1,11 +1,13 @@ import { it, describe, expect } from "vitest"; import onemacRecords from "./test-onemac-legacy.json"; -import { transformOnemacLegacy } from "../onemacLegacy"; +import { opensearch } from ".."; describe("onemac has valid data", () => { it("has valid data", () => { for (const record of onemacRecords) { - const transformedData = transformOnemacLegacy("randomid").parse(record); + const transformedData = opensearch.main.legacySubmission + .transform("randomid") + .parse(record); expect(transformedData).toHaveProperty(["attachments"]); } diff --git a/src/packages/shared-types/tests/seatool.test.ts b/src/packages/shared-types/tests/seatool.test.ts index bb9dcde983..fc0344fc81 100644 --- a/src/packages/shared-types/tests/seatool.test.ts +++ b/src/packages/shared-types/tests/seatool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import seaToolRecords from "./test-seatool.json"; -import { seatoolSchema, transformSeatoolData } from "../seatool"; +import { seatoolSchema, opensearch } from "shared-types"; describe("seatool has valid data", () => { it("can be validated against schema", () => { @@ -11,8 +11,9 @@ describe("seatool has valid data", () => { it("can be transformed into a new object", () => { for (const record of seaToolRecords) { - const transformedRecord = transformSeatoolData("randomid").parse(record); - + const transformedRecord = opensearch.main.seatool + .transform("randomid") + .parse(record); expect(transformedRecord.id).toEqual("randomid"); // expect(transformedRecord.planType).toEqual("Medicaid_SPA"); } diff --git a/src/packages/shared-utils/index.ts b/src/packages/shared-utils/index.ts index fcc7b63a22..c34c1e4b1e 100644 --- a/src/packages/shared-utils/index.ts +++ b/src/packages/shared-utils/index.ts @@ -1,7 +1,6 @@ export * from "./user-helper"; export * from "./s3-url-parser"; -export { isStateUser } from "./is-state-user"; -export * from "./rai-helper"; export * from "./regex"; export * from "./package-actions/getAvailableActions"; export * from "./packageCheck"; +export * from "./seatool-date-helper"; diff --git a/src/packages/shared-utils/is-state-user.ts b/src/packages/shared-utils/is-state-user.ts deleted file mode 100644 index 19ed46a0d2..0000000000 --- a/src/packages/shared-utils/is-state-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CognitoUserAttributes, STATE_ROLES } from "../shared-types"; - -export const isStateUser = (user: CognitoUserAttributes) => { - const userRoles = user["custom:cms-roles"]; - - for (const cmsRole of STATE_ROLES) { - if (userRoles?.includes(cmsRole)) { - return true; - } - } - return false; -}; diff --git a/src/packages/shared-utils/package-actions/getAvailableActions.ts b/src/packages/shared-utils/package-actions/getAvailableActions.ts index 20f433b592..0780420e47 100644 --- a/src/packages/shared-utils/package-actions/getAvailableActions.ts +++ b/src/packages/shared-utils/package-actions/getAvailableActions.ts @@ -1,17 +1,17 @@ import { CognitoUserAttributes, - OsMainSourceItem, PlanType, + opensearch, } from "../../shared-types"; import rules from "./rules"; import { PackageCheck } from "../packageCheck"; export const getAvailableActions = ( user: CognitoUserAttributes, - result: OsMainSourceItem + result: opensearch.main.Document ) => { const checks = PackageCheck(result); - return checks.planTypeIs([PlanType.MED_SPA]) + return checks.isSpa ? rules.filter((r) => r.check(checks, user)).map((r) => r.action) : []; }; diff --git a/src/packages/shared-utils/package-actions/rules.ts b/src/packages/shared-utils/package-actions/rules.ts index 95631ec472..65bc13283c 100644 --- a/src/packages/shared-utils/package-actions/rules.ts +++ b/src/packages/shared-utils/package-actions/rules.ts @@ -1,13 +1,17 @@ -import { Action, ActionRule, SEATOOL_STATUS } from "../../shared-types"; -import { isCmsUser, isStateUser } from "../user-helper"; -import { PackageCheck } from "../packageCheck"; +import { + Action, + ActionRule, + SEATOOL_STATUS, + finalDispositionStatuses, +} from "../../shared-types"; +import { isStateUser, isCmsWriteUser } from "../user-helper"; const arIssueRai: ActionRule = { action: Action.ISSUE_RAI, check: (checker, user) => checker.isInActivePendingStatus && (!checker.hasLatestRai || checker.hasRequestedRai) && - isCmsUser(user), + isCmsWriteUser(user), }; const arRespondToRai: ActionRule = { @@ -24,7 +28,7 @@ const arEnableWithdrawRaiResponse: ActionRule = { checker.isNotWithdrawn && checker.hasRaiResponse && !checker.hasEnabledRaiWithdraw && - isCmsUser(user), + isCmsWriteUser(user), }; const arDisableWithdrawRaiResponse: ActionRule = { @@ -33,7 +37,7 @@ const arDisableWithdrawRaiResponse: ActionRule = { checker.isNotWithdrawn && checker.hasRaiResponse && checker.hasEnabledRaiWithdraw && - isCmsUser(user), + isCmsWriteUser(user), }; const arWithdrawRaiResponse: ActionRule = { @@ -44,11 +48,10 @@ const arWithdrawRaiResponse: ActionRule = { checker.hasEnabledRaiWithdraw && isStateUser(user), }; - const arWithdrawPackage: ActionRule = { action: Action.WITHDRAW_PACKAGE, check: (checker, user) => - checker.isInActivePendingStatus && isStateUser(user), + !checker.hasStatus(finalDispositionStatuses) && isStateUser(user), }; export default [ diff --git a/src/packages/shared-utils/packageCheck.ts b/src/packages/shared-utils/packageCheck.ts index 9afcbf44c7..72cbf027c7 100644 --- a/src/packages/shared-utils/packageCheck.ts +++ b/src/packages/shared-utils/packageCheck.ts @@ -1,5 +1,4 @@ -import { OsMainSourceItem, PlanType, SEATOOL_STATUS } from "../shared-types"; -import { getLatestRai } from "./rai-helper"; +import { opensearch, PlanType, SEATOOL_STATUS } from "../shared-types"; const secondClockStatuses = [ SEATOOL_STATUS.PENDING, @@ -21,11 +20,12 @@ const checkStatus = (seatoolStatus: string, authorized: string | string[]) => * for business logic. */ export const PackageCheck = ({ seatoolStatus, - rais, + raiRequestedDate, + raiReceivedDate, + raiWithdrawnDate, raiWithdrawEnabled, planType, -}: OsMainSourceItem) => { - const latestRai = getLatestRai(rais); +}: opensearch.main.Document) => { const planChecks = { isSpa: checkPlan(planType, [PlanType.MED_SPA, PlanType.CHIP_SPA]), isWaiver: checkPlan(planType, []), @@ -43,7 +43,7 @@ export const PackageCheck = ({ isInSecondClock: !planChecks.planTypeIs([PlanType.CHIP_SPA]) && checkStatus(seatoolStatus, secondClockStatuses) && - latestRai?.status === "received", + raiRequestedDate && raiReceivedDate && !raiWithdrawnDate, /** Is in any status except Package Withdrawn **/ isNotWithdrawn: !checkStatus(seatoolStatus, SEATOOL_STATUS.WITHDRAWN), /** Added for elasticity, but common checks should always bubble up as @@ -53,11 +53,11 @@ export const PackageCheck = ({ }; const raiChecks = { /** Latest RAI is requested and status is Pending-RAI **/ - hasRequestedRai: latestRai?.status === "requested", + hasRequestedRai: !!raiRequestedDate && !raiReceivedDate && !raiWithdrawnDate, /** Latest RAI is not null **/ - hasLatestRai: latestRai !== null, + hasLatestRai: !!raiRequestedDate, /** Latest RAI has been responded to **/ - hasRaiResponse: latestRai?.status === "received", + hasRaiResponse: !!raiRequestedDate && !!raiReceivedDate && !raiWithdrawnDate, /** RAI Withdraw has been enabled **/ hasEnabledRaiWithdraw: raiWithdrawEnabled, }; diff --git a/src/packages/shared-utils/rai-helper.ts b/src/packages/shared-utils/rai-helper.ts deleted file mode 100644 index ad09de6913..0000000000 --- a/src/packages/shared-utils/rai-helper.ts +++ /dev/null @@ -1,34 +0,0 @@ -export type LatestRai = { - key: number; - status: RaiStatus; - value: any; -}; -export const getLatestRai = (rais: any | undefined): LatestRai | null => { - if (!rais || Object.keys(rais).length === 0) { - // No keys = no rai entries - return null; - } else { - const maxKey = Object.keys(rais).reduce( - (max, key) => Math.max(max, Number(key)), - -Infinity - ); - return { - key: maxKey, - status: getRaiStatus(rais[maxKey]), - value: rais[maxKey], - }; - } -}; - -export const getRaiStatus = (rai: any) => { - if (rai.withdrawnDate) { - return "withdrawn"; - } else if (rai.receivedDate) { - return "received"; - } else if (rai.requestedDate) { - return "requested"; - } else { - return "unknown"; - } -}; -export type RaiStatus = ReturnType; diff --git a/src/packages/shared-utils/seatool-date-helper.ts b/src/packages/shared-utils/seatool-date-helper.ts new file mode 100644 index 0000000000..dd7ea2c255 --- /dev/null +++ b/src/packages/shared-utils/seatool-date-helper.ts @@ -0,0 +1,21 @@ +import moment from "moment-timezone"; + +// This manually accounts for the offset between the client's timezone and UTC. +export const offsetForUtc = (date: Date): Date => { + return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); +} + +// This creates a Date for midnight today, then accounts for timezone offset. +export const seaToolFriendlyTimestamp = (date?: Date): number => { + // If you don't pass a date, we assume you want today the timestamp for today, midnight, utc. + if(!date) { + date = new Date(); + date.setHours(0,0,0,0); + } + return offsetForUtc(date).getTime(); +}; + +// This takes an epoch string and converts it to a standard format for display +export const formatSeatoolDate = (date: string): string => { + return moment(date).tz("UTC").format("MM/DD/yyyy") +} \ No newline at end of file diff --git a/src/packages/shared-utils/user-helper.ts b/src/packages/shared-utils/user-helper.ts index b0f3df46be..037502b436 100644 --- a/src/packages/shared-utils/user-helper.ts +++ b/src/packages/shared-utils/user-helper.ts @@ -1,4 +1,10 @@ -import { CMS_ROLES, CMS_WRITE_ROLES, CMS_READ_ONLY_ROLES, CognitoUserAttributes, STATE_ROLES } from "../shared-types"; +import { + CMS_ROLES, + CMS_WRITE_ROLES, + CMS_READ_ONLY_ROLES, + CognitoUserAttributes, + STATE_ROLES, +} from "../shared-types"; export const isCmsUser = (user: CognitoUserAttributes) => { const userRoles = user["custom:cms-roles"]; @@ -31,16 +37,19 @@ export const isCmsReadonlyUser = (user: CognitoUserAttributes) => { } } return false; -} +}; -export const isStateUser = (user: CognitoUserAttributes) => { - const userRoles = user["custom:cms-roles"]; +export const isStateUser = (user: CognitoUserAttributes | null) => { + if (!user) { + return false; + } else { + const userRoles = user["custom:cms-roles"]; - for (const role of STATE_ROLES) { - if (userRoles.includes(role)) { - return true; + for (const role of STATE_ROLES) { + if (userRoles.includes(role)) { + return true; + } } + return false; } - return false; - -} +}; diff --git a/src/services/api/handlers/action.ts b/src/services/api/handlers/action.ts index 9c8ae42e7f..b67fe7e5d0 100644 --- a/src/services/api/handlers/action.ts +++ b/src/services/api/handlers/action.ts @@ -75,7 +75,7 @@ export const handler = async (event: APIGatewayEvent) => { await issueRai(body); break; case Action.RESPOND_TO_RAI: - await respondToRai(body, result._source.rais); + await respondToRai(body, result._source); break; case Action.ENABLE_RAI_WITHDRAW: await toggleRaiResponseWithdraw(body, true); @@ -84,7 +84,7 @@ export const handler = async (event: APIGatewayEvent) => { await toggleRaiResponseWithdraw(body, false); break; case Action.WITHDRAW_RAI: - await withdrawRai(body, result._source.rais); + await withdrawRai(body, result._source); break; default: throw `No ${actionType} action available`; diff --git a/src/services/api/handlers/getAllForms.ts b/src/services/api/handlers/getAllForms.ts new file mode 100644 index 0000000000..e274d3460c --- /dev/null +++ b/src/services/api/handlers/getAllForms.ts @@ -0,0 +1,65 @@ +import { response } from "../libs/handler"; +import * as fs from "fs"; +import * as path from "path"; + +interface ObjectWithArrays { + [key: string]: string[]; +} + +export function removeTsAndJsExtentions( + obj: ObjectWithArrays +): ObjectWithArrays { + const result: ObjectWithArrays = {}; + + for (const key in obj) { + // eslint-disable-next-line no-prototype-builtins + if (obj.hasOwnProperty(key)) { + const filteredFiles = obj[key].filter((file) => !file.endsWith(".ts")); + result[key] = filteredFiles.map((f) => + f.replace(".js", "").replace("v", "") + ); + } + } + + return result; +} + +function getAllFormsAndVersions(directoryPath: string) { + const result: ObjectWithArrays = {}; + + const subDirectories = fs.readdirSync(directoryPath); + + subDirectories.forEach((subDir) => { + const subDirPath = path.join(directoryPath, subDir); + + if (fs.statSync(subDirPath).isDirectory()) { + const files = fs.readdirSync(subDirPath); + result[subDir] = files; + } + }); + + return removeTsAndJsExtentions(result); +} + +export const getAllForms = async () => { + try { + const filePath = getAllFormsAndVersions("/opt/"); + + if (filePath) { + return response({ + statusCode: 200, + body: filePath, + }); + } + } catch (error: any) { + console.error("Error:", error); + return response({ + statusCode: 502, + body: JSON.stringify({ + error: error.message ? error.message : "Internal server error", + }), + }); + } +}; + +export const handler = getAllForms; diff --git a/src/services/api/handlers/getAttachmentUrl.ts b/src/services/api/handlers/getAttachmentUrl.ts index 45cd592dae..ee17254adb 100644 --- a/src/services/api/handlers/getAttachmentUrl.ts +++ b/src/services/api/handlers/getAttachmentUrl.ts @@ -4,9 +4,8 @@ import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import * as os from "./../../../libs/opensearch-lib"; import { getStateFilter } from "../libs/auth/user"; -import { OsMainSourceItem, OsResponse } from "shared-types"; +import { getPackage, getPackageChangelog } from "../libs/package"; if (!process.env.osDomain) { throw "ERROR: osDomain env variable is required,"; } @@ -29,52 +28,37 @@ export const handler = async (event: APIGatewayEvent) => { try { const body = JSON.parse(event.body); - let query: any = {}; - query = { - query: { - bool: { - must: [ - { - ids: { - values: [body.id], - }, - }, - ], - }, - }, - }; - const stateFilter = await getStateFilter(event); - if (stateFilter) { - query.query.bool.must.push(stateFilter); - } - - const results = (await os.search( - process.env.osDomain, - "main", - query - )) as OsResponse; - - if (!results) { + const mainResult = await getPackage(body.id); + if (!mainResult) { return response({ statusCode: 404, body: { message: "No record found for the given id" }, }); } - const allAttachments = [ - ...(results.hits.hits[0]._source.attachments || []), - ...Object.values(results.hits.hits[0]._source.rais).flatMap((entry) => [ - ...(entry.request?.attachments || []), - ...(entry.response?.attachments || []), - ...(entry.withdraw?.attachments || []), - ]), - ]; - - if ( - !allAttachments.some((e) => { - return e.bucket === body.bucket && e.key === body.key; - }) - ) { + const stateFilter = await getStateFilter(event); + if (stateFilter) { + const stateAccessAllowed = stateFilter?.terms.state.includes( + mainResult?._source?.state?.toLocaleLowerCase() || "" + ); + + if (!stateAccessAllowed) { + return response({ + statusCode: 404, + body: { message: "state access not permitted for the given id" }, + }); + } + } + + // add state + // Do we want to check + const changelogs = await getPackageChangelog(body.id); + const attachmentExists = changelogs.hits.hits.some((CL) => { + return CL._source.attachments?.some( + (ATT) => ATT.bucket === body.bucket && ATT.key === body.key + ); + }); + if (!attachmentExists) { return response({ statusCode: 500, body: { @@ -136,6 +120,7 @@ async function getClient(bucket: string) { } } +//TODO: add check for resource before signing URL async function generatePresignedUrl( bucket: string, key: string, diff --git a/src/services/api/handlers/getPackageActions.ts b/src/services/api/handlers/getPackageActions.ts index 3e14f1de5d..5927fd1a56 100644 --- a/src/services/api/handlers/getPackageActions.ts +++ b/src/services/api/handlers/getPackageActions.ts @@ -41,6 +41,7 @@ export const getPackageActions = async (event: APIGatewayEvent) => { authDetails.userId, authDetails.poolId ); + return response({ statusCode: 200, body: { diff --git a/src/services/api/handlers/item.ts b/src/services/api/handlers/item.ts index 55ca1543c7..de71ae87ab 100644 --- a/src/services/api/handlers/item.ts +++ b/src/services/api/handlers/item.ts @@ -1,8 +1,7 @@ import { response } from "../libs/handler"; import { APIGatewayEvent } from "aws-lambda"; import { getStateFilter } from "../libs/auth/user"; -import { getPackage } from "../libs/package/getPackage"; - +import { getPackage, getPackageChangelog } from "../libs/package"; if (!process.env.osDomain) { throw "ERROR: osDomain env variable is required,"; } @@ -17,13 +16,13 @@ export const getItemData = async (event: APIGatewayEvent) => { try { const body = JSON.parse(event.body); const stateFilter = await getStateFilter(event); - const result = await getPackage(body.id); - + const packageResult = await getPackage(body.id); + const changelog = await getPackageChangelog(body.id); if ( stateFilter && - (!result._source.state || + (!packageResult._source.state || !stateFilter.terms.state.includes( - result._source.state.toLocaleLowerCase() + packageResult._source.state.toLocaleLowerCase() )) ) { return response({ @@ -32,17 +31,20 @@ export const getItemData = async (event: APIGatewayEvent) => { }); } - if (!result.found) { + if (!packageResult.found) { return response({ statusCode: 404, body: { message: "No record found for the given id" }, }); - } else { - return response({ - statusCode: 200, - body: result, - }); } + + return response({ + statusCode: 200, + body: { + ...packageResult, + _source: { ...packageResult._source, changelog: changelog.hits.hits }, + }, + }); } catch (error) { console.error({ error }); return response({ diff --git a/src/services/api/handlers/packageActions.ts b/src/services/api/handlers/packageActions.ts index 935f7aaa3a..e3934cdd5f 100644 --- a/src/services/api/handlers/packageActions.ts +++ b/src/services/api/handlers/packageActions.ts @@ -28,7 +28,8 @@ import { import { produceMessage } from "../libs/kafka"; import { response } from "../libs/handler"; import { SEATOOL_STATUS } from "shared-types/statusHelper"; -import { getLatestRai } from "shared-utils"; +import { seaToolFriendlyTimestamp } from "shared-utils"; +import { buildStatusMemoQuery } from "../libs/statusMemo"; const TOPIC_NAME = process.env.topicName as string; @@ -36,13 +37,14 @@ export async function issueRai(body: RaiIssue) { console.log("CMS issuing a new RAI"); const pool = await sql.connect(config); const transaction = new sql.Transaction(pool); + const today = seaToolFriendlyTimestamp(); try { await transaction.begin(); // Issue RAI const query1 = ` Insert into SEA.dbo.RAI (ID_Number, RAI_Requested_Date) values ('${body.id}' - ,dateadd(s, convert(int, left(${body.requestedDate}, 10)), cast('19700101' as datetime))) + ,dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime))) `; const result1 = await transaction.request().query(query1); console.log(result1); @@ -50,14 +52,21 @@ export async function issueRai(body: RaiIssue) { // Update Status const query2 = ` UPDATE SEA.dbo.State_Plan - SET SPW_Status_ID = (Select SPW_Status_ID from SEA.dbo.SPW_Status where SPW_Status_DESC = '${SEATOOL_STATUS.PENDING_RAI}') - WHERE ID_Number = '${body.id}' + SET + SPW_Status_ID = (SELECT SPW_Status_ID FROM SEA.dbo.SPW_Status WHERE SPW_Status_DESC = '${SEATOOL_STATUS.PENDING_RAI}'), + Status_Date = dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) + WHERE ID_Number = '${body.id}' `; const result2 = await transaction.request().query(query2); console.log(result2); + const statusMemoUpdate = await transaction + .request() + .query(buildStatusMemoQuery(body.id, "RAI Issued")); + console.log(statusMemoUpdate); + // write to kafka here - const result = raiIssueSchema.safeParse(body); + const result = raiIssueSchema.safeParse({ ...body, requestedDate: today }); if (result.success === false) { console.log( "RAI Validation Error. The following record failed to parse: ", @@ -69,7 +78,10 @@ export async function issueRai(body: RaiIssue) { await produceMessage( TOPIC_NAME, body.id, - JSON.stringify({ ...result.data, actionType: Action.ISSUE_RAI }) + JSON.stringify({ + ...result.data, + actionType: Action.ISSUE_RAI, + }) ); } @@ -86,28 +98,32 @@ export async function issueRai(body: RaiIssue) { } } -export async function withdrawRai(body: RaiWithdraw, rais: any) { - const activeKey = getLatestRai(rais)?.key; +export async function withdrawRai(body: RaiWithdraw, document: any) { + const raiToWithdraw = + !!document.raiRequestedDate && !!document.raiReceivedDate + ? new Date(document.raiRequestedDate).getTime() + : null; + if (!raiToWithdraw) throw "No RAI available for response"; + const today = seaToolFriendlyTimestamp(); const result = raiWithdrawSchema.safeParse({ ...body, - requestedDate: activeKey, + requestedDate: raiToWithdraw, + withdrawnDate: today, }); console.log("Withdraw body is", body); if (result.success === true) { console.log("CMS withdrawing an RAI"); - console.log(rais); - console.log("LATEST RAI KEY: " + activeKey); + console.log("LATEST RAI KEY: " + raiToWithdraw); const pool = await sql.connect(config); const transaction = new sql.Transaction(pool); - try { await transaction.begin(); // Issue RAI const query1 = ` UPDATE SEA.dbo.RAI - SET RAI_WITHDRAWN_DATE = DATEADD(s, CONVERT(int, LEFT('${result.data.withdrawnDate}', 10)), CAST('19700101' AS DATETIME)) - WHERE ID_Number = '${result.data.id}' AND RAI_REQUESTED_DATE = DATEADD(s, CONVERT(int, LEFT('${activeKey}', 10)), CAST('19700101' AS DATETIME)) + SET RAI_WITHDRAWN_DATE = DATEADD(s, CONVERT(int, LEFT('${today}', 10)), CAST('19700101' AS DATETIME)) + WHERE ID_Number = '${result.data.id}' AND RAI_REQUESTED_DATE = DATEADD(s, CONVERT(int, LEFT('${raiToWithdraw}', 10)), CAST('19700101' AS DATETIME)) `; const result1 = await transaction.request().query(query1); console.log(result1); @@ -115,17 +131,27 @@ export async function withdrawRai(body: RaiWithdraw, rais: any) { // Update Status const query2 = ` UPDATE SEA.dbo.State_Plan - SET SPW_Status_ID = (Select SPW_Status_ID from SEA.dbo.SPW_Status where SPW_Status_DESC = '${SEATOOL_STATUS.PENDING}') + SET + SPW_Status_ID = (SELECT SPW_Status_ID FROM SEA.dbo.SPW_Status WHERE SPW_Status_DESC = '${SEATOOL_STATUS.PENDING}'), + Status_Date = dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) WHERE ID_Number = '${result.data.id}' `; const result2 = await transaction.request().query(query2); console.log(result2); + const statusMemoUpdate = await transaction + .request() + .query(buildStatusMemoQuery(result.data.id, "RAI Response Withdrawn")); + console.log(statusMemoUpdate); + // write to kafka here await produceMessage( TOPIC_NAME, result.data.id, - JSON.stringify({ ...result.data, actionType: Action.WITHDRAW_RAI }) + JSON.stringify({ + ...result.data, + actionType: Action.WITHDRAW_RAI, + }) ); // Commit transaction @@ -147,24 +173,25 @@ export async function withdrawRai(body: RaiWithdraw, rais: any) { } } -export async function respondToRai(body: RaiResponse, rais: any) { +export async function respondToRai(body: RaiResponse, document: any) { console.log("State responding to RAI"); - const latestRai = getLatestRai(rais); - if (latestRai?.status != "requested") { - throw "Latest RAI is not a candidate for response"; - } - const activeKey = latestRai.key; - console.log("LATEST RAI KEY: " + activeKey); + const raiToRespondTo = + !!document.raiRequestedDate && !document.raiReceivedDate + ? new Date(document.raiRequestedDate).getTime() + : null; + if (!raiToRespondTo) throw "No RAI available for response"; + console.log("LATEST RAI KEY: " + raiToRespondTo); const pool = await sql.connect(config); const transaction = new sql.Transaction(pool); console.log(body); + const today = seaToolFriendlyTimestamp(); try { await transaction.begin(); // Issue RAI const query1 = ` UPDATE SEA.dbo.RAI - SET RAI_RECEIVED_DATE = DATEADD(s, CONVERT(int, LEFT('${body.responseDate}', 10)), CAST('19700101' AS DATETIME)) - WHERE ID_Number = '${body.id}' AND RAI_REQUESTED_DATE = DATEADD(s, CONVERT(int, LEFT('${activeKey}', 10)), CAST('19700101' AS DATETIME)) + SET RAI_RECEIVED_DATE = DATEADD(s, CONVERT(int, LEFT('${today}', 10)), CAST('19700101' AS DATETIME)) + WHERE ID_Number = '${body.id}' AND RAI_REQUESTED_DATE = DATEADD(s, CONVERT(int, LEFT('${raiToRespondTo}', 10)), CAST('19700101' AS DATETIME)) `; const result1 = await transaction.request().query(query1); console.log(result1); @@ -172,16 +199,24 @@ export async function respondToRai(body: RaiResponse, rais: any) { // Update Status const query2 = ` UPDATE SEA.dbo.State_Plan - SET SPW_Status_ID = (Select SPW_Status_ID from SEA.dbo.SPW_Status where SPW_Status_DESC = '${SEATOOL_STATUS.PENDING}') + SET + SPW_Status_ID = (SELECT SPW_Status_ID FROM SEA.dbo.SPW_Status WHERE SPW_Status_DESC = '${SEATOOL_STATUS.PENDING}'), + Status_Date = dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) WHERE ID_Number = '${body.id}' `; const result2 = await transaction.request().query(query2); console.log(result2); + const statusMemoUpdate = await transaction + .request() + .query(buildStatusMemoQuery(body.id, "RAI Response Received")); + console.log(statusMemoUpdate); + // // write to kafka here const result = raiResponseSchema.safeParse({ ...body, - requestedDate: activeKey, + responseDate: today, + requestedDate: raiToRespondTo, }); if (result.success === false) { console.log( @@ -197,6 +232,7 @@ export async function respondToRai(body: RaiResponse, rais: any) { body.id, JSON.stringify({ ...result.data, + responseDate: today, actionType: Action.RESPOND_TO_RAI, }) ); @@ -213,7 +249,6 @@ export async function respondToRai(body: RaiResponse, rais: any) { // Close pool await pool.close(); } - } export async function withdrawPackage(body: WithdrawPackage) { @@ -233,11 +268,14 @@ export async function withdrawPackage(body: WithdrawPackage) { }); } // Begin query (data is confirmed) + const today = seaToolFriendlyTimestamp(); const pool = await sql.connect(config); const transaction = new sql.Transaction(pool); const query = ` UPDATE SEA.dbo.State_Plan - SET SPW_Status_ID = (Select SPW_Status_ID from SEA.dbo.SPW_Status where SPW_Status_DESC = '${SEATOOL_STATUS.WITHDRAWN}') + SET + SPW_Status_ID = (SELECT SPW_Status_ID FROM SEA.dbo.SPW_Status WHERE SPW_Status_DESC = '${SEATOOL_STATUS.WITHDRAWN}'), + Status_Date = dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) WHERE ID_Number = '${body.id}' `; @@ -245,6 +283,10 @@ export async function withdrawPackage(body: WithdrawPackage) { await transaction.begin(); const txnResult = await transaction.request().query(query); console.log(txnResult); + const statusMemoUpdate = await transaction + .request() + .query(buildStatusMemoQuery(result.data.id, "Package Withdrawn")); + console.log(statusMemoUpdate); await produceMessage( TOPIC_NAME, body.id, diff --git a/src/services/api/handlers/search.ts b/src/services/api/handlers/search.ts index 813b3bd97e..850dac9917 100644 --- a/src/services/api/handlers/search.ts +++ b/src/services/api/handlers/search.ts @@ -8,6 +8,12 @@ if (!process.env.osDomain) { // Handler function to search index export const getSearchData = async (event: APIGatewayEvent) => { + if (!event.pathParameters || !event.pathParameters.index) { + return response({ + statusCode: 400, + body: { message: "Index path parameter required" }, + }); + } try { let query: any = {}; if (event.body) { @@ -22,6 +28,13 @@ export const getSearchData = async (event: APIGatewayEvent) => { query.query.bool.must.push(stateFilter); } + // Only return records originating from OneMAC + query.query.bool.must.push({ + terms: { + "origin.keyword": ["OneMAC"], + }, + }); + query.from = query.from || 0; query.size = query.size || 100; @@ -35,7 +48,11 @@ export const getSearchData = async (event: APIGatewayEvent) => { }); } - const results = await os.search(process.env.osDomain, "main", query); + const results = await os.search( + process.env.osDomain, + event.pathParameters.index as any, + query + ); return response({ statusCode: 200, body: results, diff --git a/src/services/api/handlers/submit.ts b/src/services/api/handlers/submit.ts index e26281a38a..c827e19d53 100644 --- a/src/services/api/handlers/submit.ts +++ b/src/services/api/handlers/submit.ts @@ -17,6 +17,8 @@ const config = { import { Kafka, Message } from "kafkajs"; import { PlanType, onemacSchema, transformOnemac } from "shared-types"; +import { seaToolFriendlyTimestamp } from "shared-utils"; +import { buildStatusMemoQuery } from "../libs/statusMemo"; const kafka = new Kafka({ clientId: "submit", @@ -60,23 +62,18 @@ export const submit = async (event: APIGatewayEvent) => { }); } + const today = seaToolFriendlyTimestamp(); const pool = await sql.connect(config); console.log(body); const query = ` Insert into SEA.dbo.State_Plan (ID_Number, State_Code, Region_ID, Plan_Type, Submission_Date, Status_Date, Proposed_Date, SPW_Status_ID, Budget_Neutrality_Established_Flag) values ('${body.id}' ,'${body.state}' - ,(Select Region_ID from SEA.dbo.States where State_Code = '${ - body.state - }') - ,(Select Plan_Type_ID from SEA.dbo.Plan_Types where Plan_Type_Name = '${ - body.authority - }') - ,dateadd(s, convert(int, left(${Date.now()}, 10)), cast('19700101' as datetime)) - ,dateadd(s, convert(int, left(${Date.now()}, 10)), cast('19700101' as datetime)) - ,dateadd(s, convert(int, left(${ - body.proposedEffectiveDate - }, 10)), cast('19700101' as datetime)) + ,(Select Region_ID from SEA.dbo.States where State_Code = '${body.state}') + ,(Select Plan_Type_ID from SEA.dbo.Plan_Types where Plan_Type_Name = '${body.authority}') + ,dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) + ,dateadd(s, convert(int, left(${today}, 10)), cast('19700101' as datetime)) + ,dateadd(s, convert(int, left(${body.proposedEffectiveDate}, 10)), cast('19700101' as datetime)) ,(Select SPW_Status_ID from SEA.dbo.SPW_Status where SPW_Status_DESC = 'Pending') ,0) `; @@ -84,6 +81,11 @@ export const submit = async (event: APIGatewayEvent) => { const result = await sql.query(query); console.log(result); + const statusMemoUpdate = await sql.query( + buildStatusMemoQuery(body.id, "Package Submitted") + ); + console.log(statusMemoUpdate); + await pool.close(); const eventBody = onemacSchema.safeParse(body); diff --git a/src/services/api/layers/ABP1/v2.ts b/src/services/api/layers/ABP1/v2.ts new file mode 100644 index 0000000000..d3a76da0dc --- /dev/null +++ b/src/services/api/layers/ABP1/v2.ts @@ -0,0 +1,1297 @@ +import { FormSchema } from "shared-types"; + +const ABP1: FormSchema = { + header: "ABP 1: Alternative Benefit Plan populations", + sections: [ + { + title: "Population identification", + form: [ + { + description: + "Identify and define the population that will participate in the Alternative Benefit Plan.", + slots: [ + { + rhf: "Input", + name: "alt_benefit_plan_population_name", + label: "Alternative Benefit Plan population name", + rules: { + required: "* Required", + }, + props: { placeholder: "enter name" }, + }, + ], + }, + { + description: + "Identify eligibility groups that are included in the Alternative Benefit Plan's population and that may contain individuals that meet any targeting criteria used to further define the population.", + slots: [ + { + rhf: "FieldArray", + name: "eligibility_groups", + props: { + appendText: "Add group", + }, + fields: [ + { + rhf: "Select", + name: "eligibility_group", + rules: { + required: "* Required", + }, + label: "Eligibility group", + props: { + sort: "ascending", + className: "min-w-[300px]", + options: [ + { + label: "Parents and Other Caretaker Relatives", + value: "parents_caretaker_relatives", + }, + { + label: "Transitional Medical Assistance", + value: "transitional_medical_assist", + }, + { + label: + "Extended Medicaid Due to Spousal Support Collections", + value: "extend_medicaid_spousal_support_collect", + }, + { + label: "Pregnant Women", + value: "pregnant_women", + }, + { + label: "Deemed Newborns", + value: "deemed_newborns", + }, + { + label: "Infants and Children under Age 19", + value: "infants_children_under_19", + }, + { + label: + "Children with Title IV-E Adoption Assistance, Foster Care or Guardianship Care", + value: + "children_title_IV-E_adoption_assist_foster_guardianship_care", + }, + { + label: "Former Foster Care Children", + value: "former_foster_children", + }, + { + label: "Adult Group", + value: "adult_group", + }, + { + label: "SSI Beneficiaries", + value: "ssi_beneficiaries", + }, + { + label: + "Aged, Blind and Disabled Individuals in 209(b) States", + value: "aged_blind_disabled_individuals_209b_states", + }, + { + label: + "Individuals Receiving Mandatory State Supplements", + value: + "individuals_receiving_mandatory_state_supplements", + }, + { + label: "Individuals Who Are Essential Spouses", + value: "essential_spouses", + }, + { + label: "Institutionalized Individuals Eligible in 1973", + value: "institutionalized_eligible_1973", + }, + { + label: "Blind or Disabled Individuals Eligible in 1937", + value: "blind_disabled_eligible_1937", + }, + { + label: + "Individuals Who Lost Eligibility for SSI/SSP Due to an Increase in OASDI Benefits in 1972", + value: + "lost_eligibility_SSI_SSP_increase_in_OASDI_benefits_1972", + }, + { + label: + "Individuals Eligible for SSI/SSP but for OASDI COLA increases since April, 1977", + value: + "eligible_SSI_SSP_but_for_OASDI_COLA_increases_April_1977", + }, + { + label: + "Disabled Widows and Widowers Ineligible for SSI due to Increase in OASDI", + value: + "disabled_widows_ineligible_SSI_due_to_increase_OASDI", + }, + { + label: + "Disabled Widows and Widowers Ineligible for SSI due to Early Receipt of Social Security", + value: + "disabled_widows_ineligible_SSI_due_to_early_receipt_social_security", + }, + { + label: "Working Disabled under 1619(b)", + value: "working_disabled_under_1619b", + }, + { + label: "Disabled Adult Children", + value: "disabled_adult_children", + }, + { + label: + "Optional Coverage of Parents and Other Caretaker Relatives", + value: "opt_coverage_parents_other_caretaker_relatives", + }, + { + label: + "Reasonable Classifications of Individuals under Age 21", + value: "reasonable_class_under_21", + }, + { + label: "Children with Non-IV-E Adoption Assistance", + value: "children_Non-IV-E_adoption_assistance", + }, + { + label: "Independent Foster Care Adolescents", + value: "independent_foster_care_adolescents", + }, + { + label: "Optional Targeted Low Income Children", + value: "opt_targeted_low_income_children", + }, + { + label: + "Certain Individuals Needing Treatment for Breast or Cervical Cancer", + value: + "individuals_need_treatment_for_breasts_cervical_cancer", + }, + { + label: "Individuals with Tuberculosis", + value: "tuberculosis", + }, + { + label: + "Aged, Blind or Disabled Individuals Eligible for but Not Receiving Cash", + value: + "aged_blind_disabled_eligible_but_not_receiving_cash", + }, + { + label: + "Individuals Eligible for Cash except for Institutionalization", + value: "eligible_cash_except_for_institutionalization", + }, + { + label: + "Individuals Receiving Home and Community Based Services under Institutional Rules", + value: + "receiving_home_community_services_under_inst_rule", + }, + { + label: + "Optional State Supplement - 1634 States and SSI Criteria States with 1616 Agreements", + value: + "opt_state_supp_1634_states_SSI_criteria_states_1616_agreements", + }, + { + label: + "Optional State Supplement - 209(b) States and SSI Criteria States without 1616 Agreements", + value: + "opt_state_supp_209b_states_SSI_criteria_states_without_1616_agreements", + }, + { + label: + "Institutionalized Individuals Eligible under a Special Income Level", + value: "inst_eligible_under_special_income_level", + }, + { + label: "Individuals Receiving Hospice Care", + value: "hospice_care", + }, + { + label: "Qualified Disabled Children under Age 19 ", + value: "qualified_disabled_children_under_19", + }, + { + label: "Poverty Level Aged or Disabled", + value: "poverty_level_aged_disabled", + }, + { + label: "Work Incentives Eligibility Group", + value: "work_incentives_eligibility_group", + }, + { + label: "Ticket to Work Basic Group", + value: "ticket_work_basic_group", + }, + { + label: "Ticket to Work Medical Improvements Group", + value: "ticket_work_medical_imp_group", + }, + { + label: + "Family Opportunity Act Children with Disabilities", + value: "family_opportunity_act_children_disabilities", + }, + { + label: "Medically Needy Pregnant Women", + value: "med_needy_pregnant_women", + }, + { + label: "Medically Needy Children under Age 18", + value: "med_needy_children_under_18", + }, + { + label: "Medically Needy Children Age 18 through 20", + value: "med_needy_age_18_through_20", + }, + { + label: "Medically Needy Parents and Other Caretakers", + value: "med_needy_parents_caretakers", + }, + { + label: "Medically Needy Aged, Blind or Disabled", + value: "med_needy_aged_blind_disabled", + }, + { + label: + "Medically Needy Blind or Disabled Individuals Eligible in 1973", + value: "med_needy_blind_disabled_eligible_1973", + }, + ], + }, + }, + { + rhf: "Select", + name: "mandatory_voluntary", + label: "Mandatory or Voluntary", + rules: { + required: "* Required", + }, + props: { + className: "w-[200px]", + options: [ + { + label: "Mandatory", + value: "mandatory", + }, + { + label: "Voluntary", + value: "voluntary", + }, + ], + }, + }, + ], + }, + ], + }, + { + description: + "Is enrollment available for all individuals in these eligibility groups?", + slots: [ + { + rhf: "Select", + name: "is_enrollment_available", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Targeting criteria", + dependency: { + // example of conditionally hidden section + conditions: [ + { + name: "is_enrollment_available", + type: "expectedValue", + expectedValue: "no", + }, + ], + effect: { type: "show" }, + }, + form: [ + { + description: "Targeting criteria (check all that apply)", + slots: [ + { + rhf: "Checkbox", + name: "target_criteria", + rules: { + required: "* Required", + }, + props: { + options: [ + { + value: "income_standard", + label: "Income standard", + form: [ + { + description: "Income standard target", + slots: [ + { + rhf: "Radio", + name: "income_target", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "Households with income at or below the standard", + value: "income_target_below", + }, + { + label: + "Households with income above the standard", + value: "income_target_above", + }, + ], + }, + }, + ], + }, + { + description: "Income standard definition", + slots: [ + { + rhf: "Radio", + name: "income_definition", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "A percentage", + value: "income_definition_percentage", + slots: [ + { + rhf: "Radio", + name: "income_definition_percentage", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Federal poverty level", + value: "federal_poverty_level", + slots: [ + { + rhf: "Input", + props: { + icon: "%", + }, + rules: { + pattern: { + value: /^\d*\.?\d+$/, + message: + "Must be a positive percentage", + }, + required: "* Required", + }, + name: "federal_poverty_level_percentage", + label: + "Enter the federal poverty level percentage", + }, + ], + }, + { + label: "SSI federal benefit amount", + value: "ssi_federal_benefit_amount", + slots: [ + { + rhf: "Input", + name: "ssi_federal_benefit_percentage", + label: + "Enter the SSI Federal Benefit Rate percentage", + props: { + icon: "%", + }, + rules: { + pattern: { + value: /^\d*\.?\d+$/, + message: + "Must be a positive percentage", + }, + required: "* Required", + }, + }, + ], + }, + { + label: "Other", + value: "other", + slots: [ + { + rhf: "Input", + name: "other_percentage", + label: + "Enter the other percentage", + props: { + icon: "%", + }, + rules: { + pattern: { + value: /^\d*\.?\d+$/, + message: + "Must be a positive percentage", + }, + required: "* Required", + }, + }, + { + rhf: "Textarea", + name: "other_describe", + label: "Describe:", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + label: "A specific amount", + value: "income_definition_specific", + slots: [ + { + rhf: "Radio", + name: "income_definition_specific", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Statewide standard", + value: "statewide_standard", + form: [ + { + slots: [ + { + rhf: "FieldArray", + name: "income_definition_specific_statewide", + fields: [ + { + rhf: "Input", + label: "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: /^[1-9]\d*$/, + message: + "Must be a positive integer value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be a positive number, maximum of two decimals, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + { + slots: [ + { + rhf: "Checkbox", + name: "is_incremental_amount_statewide_std", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount_statewide_std", + props: { + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: "Standard varies by region", + value: "region_standard", + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_region", + props: { + appendText: "Add Region", + removeText: + "Remove Region", + }, + fields: [ + { + rhf: "Input", + name: "name_of_region", + label: "Region Name", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "region_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_region_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^[1-9]\d*$/, + message: + "Must be a positive integer value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + { + label: + "Standard varies by living arrangement", + value: "living_standard", + + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_liv_arrange", + props: { + appendText: + "Add Living Arrangement", + removeText: + "Remove living arrangement", + }, + fields: [ + { + rhf: "Input", + name: "name_of_living_arrangement", + label: + "Name of living arrangement", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "living_arrangement_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_specific_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^[1-9]\d*$/, + message: + "Must be a positive integer value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + { + label: + "Standard varies in some other way", + value: "other_standard", + + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_other", + props: { + appendText: + "Add some other way", + removeText: + "Remove some other way", + }, + fields: [ + { + rhf: "Input", + name: "name_of_group", + label: "Name", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "group_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_specific_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^[1-9]\d*$/, + message: + "Must be a positive integer value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + value: "health", + label: + "Disease, condition, diagnosis, or disorder (check all that apply)", + slots: [ + { + rhf: "Checkbox", + name: "health_conditions", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Physical disability", + value: "physical_disability", + }, + { + label: "Brain Injury", + value: "brain_injury", + }, + { + label: "HIV/AIDS", + value: "hiv_aids", + }, + { + label: "Medically frail", + value: "med_frail", + }, + { + label: "Technology dependent", + value: "technology_dependent", + }, + { label: "Autism", value: "autism" }, + { + label: "Developmental disability", + value: "dev_disability", + }, + { + label: "Intellectual disability", + value: "int_disability", + }, + { + label: "Mental illness", + value: "mental_illness", + }, + { + label: "Substance use disorder", + value: "substance_use_dis", + }, + { label: "Diabetes", value: "diabetes" }, + { label: "Heart disease", value: "heart_dis" }, + { label: "Asthma", value: "asthma" }, + { label: "Obesity", value: "obesity" }, + { + label: + "Other disease, condition, diagnosis, or disorder", + value: "other", + slots: [ + { + rhf: "Textarea", + name: "other_description", + label: "Describe", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + label: "Other targeting criteria", + value: "other_targeting_criteria", + slots: [ + { + rhf: "Textarea", + name: "other_targeting_criteria_description", + label: "Describe", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Geographic Area", + form: [ + { + description: + "Will the Alternative Benefit Plan population include individuals from the entire state/territory?", + slots: [ + { + rhf: "Select", + name: "is_geographic_area", + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + rules: { + required: "* Required", + }, + }, + ], + }, + { + description: "Method of geographic variation", + dependency: { + conditions: [ + { + name: "is_geographic_area", + type: "expectedValue", + expectedValue: "no", + }, + ], + effect: { type: "show" }, + }, + slots: [ + { + rhf: "Radio", + name: "geographic_variation", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "By county", + value: "by_county", + form: [ + { + description: "Specify counties", + slots: [ + { + name: "specify_counties", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "By region", + value: "by_region", + form: [ + { + description: "Specify regions", + slots: [ + { + name: "specify_regions", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "By city or town", + value: "by_city_town", + form: [ + { + description: "Specify cities or towns", + slots: [ + { + name: "specify_cities_towns", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "Other geographic area", + value: "other", + form: [ + { + description: "Specify other geographic area", + slots: [ + { + name: "specify_other", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Additional information", + form: [ + { + description: + "Any other information the state/territory wishes to provide about the population (optional)", + // "Other information related to selection of the Section 1937 coverage option and the base benchmark plan (optional)", + slots: [ + { + name: "additional_information", + rhf: "Textarea", + }, + ], + }, + ], + }, + // { + // title: "Testing Alt Layouts", + // form: [ + // { + // description: "A test of horizontal layouts with no slot styles", + // wrapperStyling: "flex flex-wrap gap-6", + // slots: [ + // { + // name: "example1_1", + // label: "Example 1.1", + // rhf: "Input", + // }, + // { + // name: "example1_2", + // label: "Example 1.2", + // rhf: "Input", + // }, + // { + // name: "example1_3", + // label: "Example 1.3", + // rhf: "Input", + // }, + // ], + // }, + // { + // description: "A test of horizontal layouts with slot styles", + // wrapperStyling: "flex flex-wrap gap-6", + // slots: [ + // { + // name: "example2_1", + // label: "Example 2.1", + // rhf: "Input", + // props: { + // className: "w-80", + // }, + // }, + // { + // name: "example2_2", + // label: "Example 2.2", + // rhf: "Input", + // props: { + // className: "w-30", + // }, + // }, + // { + // name: "example2_3", + // label: "Example 2.3", + // rhf: "Input", + // props: { + // className: "w-120", + // }, + // }, + // ], + // }, + // ], + // }, + ], +}; + +export const form = ABP1; diff --git a/src/services/api/layers/ABP10/v1.ts b/src/services/api/layers/ABP10/v1.ts new file mode 100644 index 0000000000..6fd4c658cc --- /dev/null +++ b/src/services/api/layers/ABP10/v1.ts @@ -0,0 +1,102 @@ +import { FormSchema } from "shared-types"; + +const ABP10: FormSchema = { + header: "ABP 10: General assurances", + sections: [ + { + title: "Economy and efficiency of plans", + form: [ + { + slots: [ + { + rhf: "Checkbox", + name: "economy_and_efficiency_of_plans", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The state or territory assures that Alternative Benefit Plan coverage is provided in accordance with federal upper payment limit requirements and other economy and efficiency principles that would otherwise be applicable to the services or delivery system through which the coverage and benefits are obtained.", + value: "assures_alternative_benefit_plan_in_accordance", + }, + ], + }, + }, + { + rhf: "Select", + name: "economy_and_efficeiency_same_approach_as_medicaid_state_plan", + label: + "Will economy and efficiency be achieved using the same approach as used for Medicaid state plan services?", + labelStyling: "font-bold", + rules: { required: "* Required" }, + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { + label: "No", + value: "no", + }, + ], + }, + }, + { + rhf: "Textarea", + label: "Describe the approach", + labelStyling: "font-bold", + name: "describe_approach", + formItemStyling: "ml-[0.6rem] px-4 border-l-4 border-l-primary", + rules: { required: "* Required" }, + dependency: { + conditions: [ + { + name: "economy_and_efficeiency_same_approach_as_medicaid_state_plan", + type: "expectedValue", + expectedValue: "no", + }, + ], + effect: { type: "show" }, + }, + }, + ], + }, + ], + }, + { + title: "Compliance with the law", + form: [ + { + slots: [ + { + rhf: "Checkbox", + name: "compliance_with_the_law", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The state or territory will continue to comply with all other provisions of the Social Security Act in the administration of the state or territory plan under this title.", + value: "comply_with_social_security_act", + }, + { + label: + "The state or territory assures that Alternative Benefit Plan benefits designs shall conform to the non-discrimination requirements at 42 CFR 430.2 and 42 CFR 440.347(e).", + value: "assures_alternative_benefit_plan_shall_conform", + }, + { + label: + "The state or territory assures that all providers of Alternative Benefit Plan benefits shall meet the provider qualification requirements of the base benchmark plan and/or the Medicaid state plan.", + value: + "providers_of_alternative_benefit_plan_meets_provider_qualifications", + }, + ], + }, + }, + ], + }, + ], + }, + ], +}; + +export const form = ABP10; diff --git a/src/services/api/layers/ABP3/v1.ts b/src/services/api/layers/ABP3/v1.ts new file mode 100644 index 0000000000..1b846b4a63 --- /dev/null +++ b/src/services/api/layers/ABP3/v1.ts @@ -0,0 +1,359 @@ +import { FormSchema } from "shared-types"; + +const ABP3: FormSchema = { + header: + "ABP 3: Selection of benchmark benefit package or benchmark-equivalent benefit package", + sections: [ + { + title: "Benefit package details", + form: [ + { + description: "Select one of the following", + slots: [ + { + rhf: "Radio", + name: "benefit_package_details", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The state/territory is amending one existing benefit package for the population defined in section 1.", + value: "benchmark_amending", + }, + { + label: + "The state/territory is creating a single new benefit package for the population defined in section 1.", + value: "benchmark_creating", + }, + ], + }, + }, + { + rhf: "Input", + name: "benefit_package_name", + label: "Benefit package name", + rules: { required: "* Required" }, + dependency: { + conditions: [ + { + name: "benefit_package_details", + type: "expectedValue", + expectedValue: "benchmark_creating", + }, + ], + effect: { type: "show" }, + }, + }, + ], + }, + ], + }, + { + title: "Selection of Section 1937 coverage option", + form: [ + { + description: + "The state/territory selects as its Section 1937 coverage option the following type of benchmark benefit package or benchmark-equivalent benefit package under this Alternative Benefit Plan:", + slots: [ + { + rhf: "Radio", + name: "section_1937_coverage_option", + rules: { required: "* Required" }, + props: { + options: [ + { + label: "Benchmark benefit package", + value: "benchmark_benefit_package", + form: [ + { + description: + "The state/territory will provide the following benchmark benefit package:", + slots: [ + { + rhf: "Radio", + name: "Benchmark_benefit_packag_options", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The standard Blue Cross Blue Shield preferred provider option offered through the Federal Employee Health Benefit Program (FEHBP)", + value: "blue_cross_blue_shield", + }, + { + label: + "State employee coverage that is offered and generally available to state employees (state employee coverage)", + value: "state_employee_coverage", + form: [ + { + slots: [ + { + rhf: "Input", + label: "Plan name", + name: "state_employee_coverage_plan_name", + rules: { required: "* Required" }, + }, + ], + }, + ], + }, + { + label: + "A commercial HMO with the largest insured commercial, non-Medicaid enrollment in the state/territory (commercial HMO)", + value: "commercial_hmo", + form: [ + { + slots: [ + { + rhf: "Input", + label: "Plan name", + name: "commercial_hmo_plan_name", + rules: { required: "* Required" }, + }, + ], + }, + ], + }, + { + label: "Secretary-approved coverage", + value: "secretary_approved_coverage", + form: [ + { + slots: [ + { + rhf: "Radio", + name: "secretary_approved_coverage_options", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The state/territory offers benefits based on the approved state plan.", + value: "approved_state_plan", + form: [ + { + slots: [ + { + rhf: "Radio", + name: "approved_state_plan_options", + rules: { + required: + "* Required", + }, + props: { + options: [ + { + label: + "The state/territory offers the benefits provided in the approved state plan.", + value: + "approved_state_plan", + }, + { + label: + "Benefits include all those provided in the approved state plan plus additional benefits.", + value: + "additional_benefits", + }, + { + label: + "Benefits are the same as provided in the approved state plan but in a different amount, duration, and/or scope.", + value: + "different_amount_duration_scope", + }, + { + label: + "The state/territory offers only a partial list of benefits provided in the approved state plan.", + value: + "partial_list_of_benefits", + }, + { + label: + "The state/territory offers a partial list of benefits provided in the approved state plan plus additional benefits.", + value: + "partial_list_of_benefits_plus_additional_benefits", + }, + ], + }, + }, + ], + }, + ], + }, + { + label: + "The state/territory offers an array of benefits from the Section 1937 coverage option and/or base benchmark plan benefit packages, the approved state plan, or a combination of these benefit packages.", + value: "array_of_benefits", + }, + ], + }, + }, + { + rhf: "Textarea", + name: "benefits_and_limitations", + rules: { required: "* Required" }, + label: + "Briefly identify the benefits, the source of benefits, and any limitations.", + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: "Benchmark-equivalent benefit package", + value: "benchmark_equivalent_benefit_package", + form: [ + { + description: + "The state/territory will provide the following benchmark-equivalent benefit package:", + slots: [ + { + rhf: "Radio", + name: "benchmark_equivalent", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The standard Blue Cross Blue Shield preferred provider option offered through the Federal Employee Health Benefit Program (FEHBP)", + value: "blue_cross_blue_shield", + }, + { + label: + "State employee coverage that is offered and generally available to state employees (state employee coverage)", + value: "state_employee_coverage", + form: [ + { + slots: [ + { + rhf: "Input", + name: "state_employee_coverage_plan_name", + label: "Plan name", + rules: { required: "* Required" }, + }, + ], + }, + ], + }, + { + label: + "A commercial HMO with the largest insured commercial, non-Medicaid enrollment in the state/territory (commercial HMO)", + value: "commercial_hmo", + form: [ + { + slots: [ + { + rhf: "Input", + name: "commercial_hmo_plan_name", + label: "Plan name", + rules: { required: "* Required" }, + }, + ], + }, + ], + }, + { + label: "Secretary-approved coverage", + value: "secretary_approved_coverage", + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Selection of base benchmark plan", + form: [ + { + description: + "The state/territory must select a base benchmark plan as the basis for providing essential health benefits in its benchmark or benchmark-equivalent package.", + slots: [ + { + rhf: "Select", + label: + "Is the base benchmark plan the same as the Section 1937 coverage option?", + name: "base_benchmark_plan_same_as_section_1937", + rules: { required: "* Required" }, + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + { + rhf: "Radio", + label: + "Indicate which benchmark plan described at 45 CFR 156.100(a) the state/territory will use as its base benchmark plan.", + name: "base_benchmark_plan", + rules: { required: "* Required" }, + props: { + options: [ + { + label: + "The largest plan by enrollment of the three largest small group insurance products in the state's small group market", + value: "largest_plan_by_enrollment", + }, + { + label: + "Any of the largest three state employee health benefit plans by enrollment", + value: "any_of_largest_three_state", + }, + { + label: + "Any of the largest three national FEHBP plan options open to federal employees in all geographies by enrollment", + value: "any_of_largest_three_national_fehbp_plan_options", + }, + { + label: "The largest insured commercial non-Medicaid HMO", + value: "largest_insured_commercial_hmo", + }, + ], + }, + }, + { + rhf: "Input", + label: "Plan name", + name: "base_benchmark_plan_name", + rules: { required: "* Required" }, + }, + ], + }, + ], + }, + { + title: "Additional information", + form: [ + { + description: + "Other information related to selection of the Section 1937 coverage option and the base benchmark plan (optional)", + slots: [ + { + rhf: "Textarea", + name: "additional_information", + }, + ], + }, + ], + }, + ], +}; + +export const form = ABP3; diff --git a/src/services/api/layers/ABP3_1/v1.ts b/src/services/api/layers/ABP3_1/v1.ts new file mode 100644 index 0000000000..38e98506d8 --- /dev/null +++ b/src/services/api/layers/ABP3_1/v1.ts @@ -0,0 +1,1789 @@ +import { FormSchema } from "shared-types"; + +const ABP3_1: FormSchema = { + header: + "ABP 3.1 Selection of benchmark benefit package or benchmark-equivalent benefit package", + sections: [ + { + title: "Benefit package details", + form: [ + { + description: "Select the following:", + slots: [ + { + rhf: "Radio", + name: "amending_benefit", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The state or territory is amending one existing benefit package for the population defined in Section 1.", + value: "existing_package", + }, + { + label: + "The state or territory is creating a single new benefit package for the population defined in Section 1.", + value: "new_package", + }, + ], + }, + }, + ], + }, + { + description: "Benefit package name", + slots: [ + { + rhf: "Input", + name: "benefit_package_name", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + title: "Selection of Section 1937 coverage option", + form: [ + { + description: + "The state/territory selects as its Section 1937 coverage option the following type of benchmark benefit package or benchmark-equivalent benefit package under this Alternative Benefit Plan:", + slots: [ + { + name: "benchmark", + rhf: "Radio", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Benchmark benefit package", + value: "benefit_package", + form: [ + { + description: + "The state/territory will provide the following benchmark benefit package:", + slots: [ + { + name: "state_territory_benchmark", + rhf: "Radio", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The standard Blue Cross Blue Shield preferred provider option offered through the Federal Employee Health Benefit Program (FEHBP)", + value: "blue_cross_blue_shield", + }, + { + label: + "State employee coverage that is offered and generally available to state employees (state employee coverage)", + value: "state_employee_coverage", + slots: [ + { + rhf: "Input", + name: "benchmark_plan_name", + label: "Plan name", + rules: { + required: "* Required", + }, + }, + ], + }, + { + label: + "A commercial HMO with the largest insured commercial, non-Medicaid enrollment in the state/territory (commercial HMO)", + value: "commerical_HMO", + slots: [ + { + rhf: "Input", + name: "benchmark_HMO_plan_name", + label: "Plan name", + rules: { + required: "* Required", + }, + }, + ], + }, + { + label: "Secretary-approved coverage", + value: "secretary_approved", + form: [ + { + slots: [ + { + name: "benefits_based_on", + rhf: "Radio", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The state/territory offers benefits based on the approved state plan.", + value: "state_plan", + form: [ + { + slots: [ + { + rhf: "Radio", + name: "state_plan_benefits", + props: { + options: [ + { + label: + "The state/territory offers the benefits provided in the approved state plan.", + value: + "provided_in_approved_state_plan", + }, + { + label: + "Benefits include all those provided in the approved state plan plus additional benefits.", + value: + "additional_benefits", + }, + { + label: + "Benefits are the same as provided in the approved state plan but in a different amount, duration, and/or scope.", + value: + "different_amount_duration_scope", + }, + { + label: + "The state/territory offers only a partial list of benefits provided in the approved state plan.", + value: + "partial_list", + }, + { + label: + "The state/territory offers a partial list of benefits provided in the approved state plan plus additional benefits.", + value: + "partial_list_plus_additional_benefits", + }, + ], + }, + rules: { + required: + "* Required", + }, + }, + ], + }, + ], + }, + { + label: + "The state/territory offers an array of benefits from the Section 1937 coverage option and/or base benchmark plan benefit packages, the approved state plan, or a combination of these benefit packages.", + value: "array_of_benefits", + }, + ], + }, + }, + { + rhf: "Textarea", + name: "benefits__description", + label: + "Briefly identify the benefits, the source of benefits, and any limitations.", + labelStyling: "font-bold", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: "Benchmark-equivalent benefit package", + value: "equivalent", + form: [ + { + description: + "The state/territory will provide the following benchmark-equivalent benefit package:", + slots: [ + { + rhf: "Radio", + name: "geographic_variation", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The standard Blue Cross Blue Shield preferred provider option offered through the Federal Employee Health Benefit Program (FEHBP)", + value: "blue_cross_blue_shield", + }, + { + label: + "State employee coverage that is offered and generally available to state employees (state employee coverage)", + value: "state_employee_coverage", + slots: [ + { + rhf: "Input", + name: "benchmark_equivalent_state_coverage_plan_name", + label: "Plan name", + rules: { + required: "* Required", + }, + }, + ], + }, + { + label: + "A commercial HMO with the largest insured commercial, non-Medicaid enrollment in the state/territory (commercial HMO)", + value: "commerical_HMO", + slots: [ + { + rhf: "Input", + name: "benchmark_equivalent_HMO_plan_name", + label: "Plan name", + rules: { + required: "* Required", + }, + }, + ], + }, + { + label: + "The Medicaid state plan coverage provided to Categorically Needy (Mandatory and Options for Coverage) eligibility groups", + value: "state_plan_to_categorically_needy", + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + + { + title: "Selection of EHB-benchmark plan", + form: [ + { + description: + "The state or territory must select an EHB-benchmark plan as the basis for providing essential health benefits in its benchmark or benchmark-equivalent package.", + slots: [ + { + name: "EHB_benchmark_name", + rhf: "Input", + label: "EHB-benchmark plan name", + labelStyling: "font-bold", + rules: { + required: "* Required", + }, + }, + + { + name: "is_EHB_benchmark_plan_same_section_1937", + rhf: "Select", + label: + "Is the EHB-benchmark plan the same as the Section 1937 coverage option?", + labelStyling: "font-bold", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + + { + label: + "Indicate the EHB-benchmark option as described at 45 CFR 156.111(b)(2)(B) the state or territory will use as its EHB-benchmark plan.", + labelStyling: "font-bold", + name: "EHB_benchmark_option", + rhf: "Radio", + description: + "State or territory is selecting one of the below options to design an EHB package that complies with the requirements for the individual insurance market under 45 CFR 156.100 through 156.125.", + descriptionAbove: true, + descriptionStyling: "text-base text-black", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "State/territory is selecting the EHB-benchmark plan used by the state/territory for the 2017 plan year.", + value: "EHB_benchmark_2017_plan_year", + }, + { + label: + "State/territory is selecting one of the EHB-benchmark plans used for the 2017 plan year by another state/territory.", + value: "another_state_EHB_benchmark_plan_year", + slots: [ + { + rhf: "Select", + name: "is_geographic_area", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + rules: { + required: "* Required", + }, + }, + { + rhf: "Radio", + name: "indicate_EHB_bencmark_plan", + label: "Indicate the type of EHB-benchmark plan.", + labelStyling: "font-bold", + props: { + options: [ + { + label: + "The largest plan by enrollment of the three largest small group insurance products in the state's small group market", + value: "three_largest_small_group_insurance", + }, + { + label: + "Any of the largest three state employee health benefit plans by enrollment", + value: "largest_three_state_employee_plans", + }, + { + label: + "Any of the largest three national FEHBP plan options open to Federal employees in all geographies by enrollment", + value: "largest_three_state_FEHBP_plans", + }, + { + label: + "The largest insured commercial non-Medicaid HMO", + value: "larged_insured_commercial", + }, + ], + }, + rules: { + required: "* Required", + }, + }, + ], + }, + { + label: + "State/territory selects the following EHB-benchmark plan used for the 2017 plan year but will replace coverage of one or more of the categories of EHB with coverage of the same category from the 2017 EHB-benchmark plan of one or more other states.", + value: "EHB_benchmark_2017_plan_year_but_replace_coverage", + form: [ + { + description: "Indicate the type of EHB-benchmark plan.", + slots: [ + { + rhf: "Radio", + name: "EHB-benchmark_plan", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The largest plan by enrollment of the three largest small group insurance products in the state's small group market", + value: "three_largest_small_group_insurance", + }, + { + label: + "Any of the largest three state employee health benefit plans by enrollment", + value: "largest_three_state_employee_plans", + }, + { + label: + "Any of the largest three national FEHBP plan options open to Federal employees in all geographies by enrollment", + value: "largest_three_state_FEHBP_plans", + }, + { + label: + "The largest insured commercial non-Medicaid HMO", + value: "larged_insured_commercial", + }, + ], + }, + }, + { + rhf: "Checkbox", + name: "one_or_more_EHBs_other_states", + label: "Select one or more EHBs from other states.", + labelStyling: "font-bold", + rules: { required: "* Required" }, + props: { + options: [ + { + label: "Ambulatory patient services", + value: "ambulatory_patient_services", + slots: [ + { + rhf: "Select", + name: "ambulatory_patient_services_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: "Emergency services", + value: "emergency_services", + slots: [ + { + rhf: "Select", + name: "emergency_services_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: "Hospitalization", + value: "hospitalization", + slots: [ + { + rhf: "Select", + name: "hospitalization_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: "Maternity and newborn care", + value: "maternity_and_newborn_care", + slots: [ + { + rhf: "Select", + name: "maternity_and_newborn_care_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: + "Mental health and substance use disorders", + value: + "mental_health_and_substance_use_disorders", + slots: [ + { + rhf: "Select", + name: "mental_health_and_substance_use_disorders_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: "Prescription drugs", + value: "prescription_drugs", + slots: [ + { + rhf: "Select", + name: "prescription_drugs_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: + "Rehabilitative and habilitative services and devices", + value: + "rehabilitative_and_habilitative_services_and_devices", + slots: [ + { + rhf: "Select", + name: "rehabilitative_and_habilitative_services_and_devices_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: "Laboratory services", + value: "laboratory_services", + slots: [ + { + rhf: "Select", + name: "laboratory_services_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: + "Preventive and wellness services and chronic disease management", + value: + "preventive_and_wellness_services_and_chronic_disease_management", + slots: [ + { + rhf: "Select", + name: "preventive_and_wellness_services_and_chronic_disease_management_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + { + label: + "Pediatric services, including oral and vision care", + value: + "pediatric_services_including_oral_and_vision_care", + slots: [ + { + rhf: "Select", + name: "ediatric_services_including_oral_and_vision_care_state_territory", + label: "Identify the state/territory", + labelStyling: "p-3", + formItemStyling: "flex-row", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Alabama", value: "AL" }, + { label: "Alaska", value: "AK" }, + { + label: "American Samoa", + value: "AS", + }, + { label: "Arizona", value: "AZ" }, + { label: "Arkansas", value: "AR" }, + { label: "California", value: "CA" }, + { label: "Colorado", value: "CO" }, + { label: "Connecticut", value: "CT" }, + { label: "Delaware", value: "DE" }, + { + label: "District of Columbia", + value: "DC", + }, + { label: "Florida", value: "FL" }, + { label: "Georgia", value: "GA" }, + { label: "Guam", value: "GU" }, + { label: "Hawaii", value: "HI" }, + { label: "Idaho", value: "ID" }, + { label: "Illinois", value: "IL" }, + { label: "Indiana", value: "IN" }, + { label: "Iowa", value: "IA" }, + { label: "Kansas", value: "KS" }, + { label: "Kentucky", value: "KY" }, + { label: "Louisiana", value: "LA" }, + { label: "Maine", value: "ME" }, + { label: "Maryland", value: "MD" }, + { + label: "Massachusetts", + value: "MA", + }, + { label: "Michigan", value: "MI" }, + { label: "Minnesota", value: "MN" }, + { label: "Mississippi", value: "MS" }, + { label: "Missouri", value: "MO" }, + { label: "Montana", value: "MT" }, + { label: "Nebraska", value: "NE" }, + { label: "Nevada", value: "NV" }, + { + label: "New Hampshire", + value: "NH", + }, + { label: "New Jersey", value: "NJ" }, + { label: "New Mexico", value: "NM" }, + { label: "New York", value: "NY" }, + { + label: "North Carolina", + value: "NC", + }, + { + label: "North Dakota", + value: "ND", + }, + { + label: "Northern Mariana Islands", + value: "MP", + }, + { label: "Ohio", value: "OH" }, + { label: "Oklahoma", value: "OK" }, + { label: "Oregon", value: "OR" }, + { + label: "Pennsylvania", + value: "PA", + }, + { label: "Puerto Rico", value: "PR" }, + { + label: "Rhode Island", + value: "RI", + }, + { + label: "South Carolina", + value: "SC", + }, + { + label: "South Dakota", + value: "SD", + }, + { label: "Tennessee", value: "TN" }, + { label: "Texas", value: "TX" }, + { label: "Utah", value: "UT" }, + { label: "Vermont", value: "VT" }, + { + label: "Virgin Islands", + value: "VI", + }, + { label: "Virginia", value: "VA" }, + { label: "Washington", value: "WA" }, + { + label: "West Virginia", + value: "WV", + }, + { label: "Wisconsin", value: "WI" }, + { label: "Wyoming", value: "WY" }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: + "Select a set of benefits consistent with the 10 EHB categories to become the new EHB-benchmark plan. (Complete and submit the ABP 5: Benefits Description form to describe the set of benefits.)", + value: "10_EHB_categories_new_EHB_benchmark", + }, + ], + }, + }, + ], + }, + ], + }, + + { + title: "Assurances", + form: [ + { + slots: [ + { + name: "assurances_meets_scope", + rhf: "Checkbox", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "The state/territory assures the EHB plan meets the scope of benefits standards at 45 CFR 156.111(b), does not exceed generosity of most generous among a set of comparison plans, provides appropriate balance of coverage among 10 EHB categories, and the scope of benefits is equal to or greater than the scope of benefits provided under a typical employer plan as defined at 45 CFR 156.111(b)(2).", + value: "meets_scope", + }, + { + label: + "The state/territory assures that actuarial certification and an associated actuarial report from an actuary, who is a member of the American Academy of Actuaries, in accordance with generally accepted actuarial principles and methodologies, has been completed and is available upon request.", + value: "assures_from_acturial_certification", + dependency: { + conditions: [ + { + name: "EHB_benchmark_option", + type: "expectedValue", + expectedValue: "EHB_benchmark_2017_plan_year", + }, + ], + effect: { type: "hide" }, + }, + }, + { + label: + "The state/territory assures that all services in the EHB-benchmark plan have been accounted for throughout the benefit chart found in ABP 5.", + value: "assures_EHB_benchmark_throughout_abp5_benefit", + }, + { + label: + "The state/territory assures the accuracy of all information in ABP 5 depicting amount, duration, and scope parameters of services authorized in the currently approved Medicaid state plan.", + value: "assures_abp5_amount_duration_scope", + }, + ], + }, + }, + ], + }, + ], + }, + + { + title: "Additional information", + form: [ + { + description: + "Other information related to selection of the Section 1937 coverage option and the EHB-benchmark plan (optional)", + slots: [ + { + name: "additional_information", + rhf: "Textarea", + }, + ], + }, + ], + }, + ], +}; + +export const form = ABP3_1; diff --git a/src/services/api/layers/tsconfig.json b/src/services/api/layers/tsconfig.json index bbda01c81f..38a9974ea0 100644 --- a/src/services/api/layers/tsconfig.json +++ b/src/services/api/layers/tsconfig.json @@ -3,7 +3,8 @@ "target": "ES2016", "moduleResolution": "node", "module": "commonjs", - "skipLibCheck": true + "skipLibCheck": true, + "esModuleInterop": true }, "include": ["./**/*.ts"], "exclude": ["node_modules"] diff --git a/src/services/api/libs/auth/user.ts b/src/services/api/libs/auth/user.ts index 1fdd13690f..fb1c8ede46 100644 --- a/src/services/api/libs/auth/user.ts +++ b/src/services/api/libs/auth/user.ts @@ -123,6 +123,7 @@ export const isAuthorizedToGetPackageActions = async ( ); }; +// originally intended for /_search export const getStateFilter = async (event: APIGatewayEvent) => { // Retrieve authentication details of the user const authDetails = getAuthDetails(event); @@ -137,11 +138,14 @@ export const getStateFilter = async (event: APIGatewayEvent) => { if (userAttributes["custom:state"]) { const filter = { terms: { + //NOTE: this could instead be + // "state.keyword": userAttributes["custom:state"], state: userAttributes["custom:state"] .split(",") .map((state) => state.toLocaleLowerCase()), }, }; + return filter; } else { throw "State user detected, but no associated states. Cannot continue"; diff --git a/src/services/api/libs/package/changelog.ts b/src/services/api/libs/package/changelog.ts new file mode 100644 index 0000000000..2f17293645 --- /dev/null +++ b/src/services/api/libs/package/changelog.ts @@ -0,0 +1,22 @@ +import * as os from "../../../../libs/opensearch-lib"; +import { opensearch } from "shared-types"; + +export const getPackageChangelog = async ( + packageId: string, + filter: any[] = [] +) => { + if (!process.env.osDomain) { + throw new Error("process.env.osDomain must be defined"); + } + + return (await os.search(process.env.osDomain, "changelog", { + from: 0, + size: 200, + sort: [{ timestamp: "desc" }], + query: { + bool: { + must: [{ term: { "packageId.keyword": packageId } }].concat(filter), + }, + }, + })) as opensearch.changelog.Response; +}; diff --git a/src/services/api/libs/package/getPackage.ts b/src/services/api/libs/package/getPackage.ts index 887000a4f7..92c824667f 100644 --- a/src/services/api/libs/package/getPackage.ts +++ b/src/services/api/libs/package/getPackage.ts @@ -1,9 +1,13 @@ import * as os from "../../../../libs/opensearch-lib"; -import { ItemResult } from "shared-types"; +import { opensearch } from "shared-types"; export const getPackage = async (id: string) => { if (!process.env.osDomain) { throw new Error("process.env.osDomain must be defined"); } - return (await os.getItem(process.env.osDomain, "main", id)) as ItemResult; + return (await os.getItem( + process.env.osDomain, + "main", + id + )) as opensearch.main.ItemResult; }; diff --git a/src/services/api/libs/package/index.ts b/src/services/api/libs/package/index.ts new file mode 100644 index 0000000000..a82b6b308d --- /dev/null +++ b/src/services/api/libs/package/index.ts @@ -0,0 +1,2 @@ +export * from "./changelog"; +export * from "./getPackage"; diff --git a/src/services/api/libs/statusMemo.ts b/src/services/api/libs/statusMemo.ts new file mode 100644 index 0000000000..ba55b1a6b0 --- /dev/null +++ b/src/services/api/libs/statusMemo.ts @@ -0,0 +1,8 @@ +export function buildStatusMemoQuery(id: string, msg: string) { + return ` + UPDATE SEA.dbo.State_Plan + SET + Status_Memo = '- OneMAC Activity: ' + FORMAT(DATEADD(SECOND, CAST('${Date.now()}' AS BIGINT) / 1000, '1970-01-01'), 'MM/dd/yyyy HH:mm') + ' - ' + '${msg} ' + '\r' + CAST(ISNULL(Status_Memo, '') AS VARCHAR(MAX)) + WHERE ID_Number = '${id}' + `; +} diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index 38b30a7db1..fde8717963 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -75,24 +75,28 @@ custom: params: master: formsProvisionedConcurrency: 2 + getAllFormsProvisionedConcurrency: 1 searchProvisionedConcurrency: 4 itemProvisionedConcurrency: 2 getAttachmentUrlProvisionedConcurrency: 2 submitProvisionedConcurrency: 2 val: formsProvisionedConcurrency: 2 + getAllFormsProvisionedConcurrency: 1 searchProvisionedConcurrency: 4 itemProvisionedConcurrency: 2 getAttachmentUrlProvisionedConcurrency: 2 submitProvisionedConcurrency: 2 production: formsProvisionedConcurrency: 5 + getAllFormsProvisionedConcurrency: 1 searchProvisionedConcurrency: 10 itemProvisionedConcurrency: 5 getAttachmentUrlProvisionedConcurrency: 5 submitProvisionedConcurrency: 5 default: formsProvisionedConcurrency: 0 + getAllFormsProvisionedConcurrency: 1 searchProvisionedConcurrency: 0 itemProvisionedConcurrency: 0 getAttachmentUrlProvisionedConcurrency: 0 @@ -107,7 +111,7 @@ functions: osDomain: ${param:osDomain} events: - http: - path: /search + path: /search/{index} method: post cors: true authorizer: aws_iam @@ -244,7 +248,25 @@ functions: path: /forms method: get cors: true - authorizer: aws_iam + vpc: + securityGroupIds: + - Ref: SecurityGroup + subnetIds: >- + ${self:custom.vpc.privateSubnets} + provisionedConcurrency: ${param:searchProvisionedConcurrency} + getAllForms: + handler: handlers/getAllForms.handler + layers: + - !Ref FormsLambdaLayer + maximumRetryAttempts: 0 + environment: + region: ${self:provider.region} + osDomain: ${param:osDomain} + events: + - http: + path: /allForms + method: get + cors: true vpc: securityGroupIds: - Ref: SecurityGroup diff --git a/src/services/auth/libs/users.json b/src/services/auth/libs/users.json index b810bc9e50..f6439f9c62 100644 --- a/src/services/auth/libs/users.json +++ b/src/services/auth/libs/users.json @@ -28,6 +28,35 @@ } ] }, + { + "username": "automated-state@example.com", + "attributes": [ + { + "Name": "email", + "Value": "automated-state@example.com" + }, + { + "Name": "given_name", + "Value": "Otto" + }, + { + "Name": "family_name", + "Value": "State" + }, + { + "Name": "email_verified", + "Value": "true" + }, + { + "Name": "custom:state", + "Value": "TX,CA,NY,FL" + }, + { + "Name": "custom:cms-roles", + "Value": "onemac-micro-statesubmitter" + } + ] + }, { "username": "readonly@example.com", "attributes": [ @@ -57,6 +86,35 @@ } ] }, + { + "username": "automated-readonly@example.com", + "attributes": [ + { + "Name": "email", + "Value": "automated-readonly@example.com" + }, + { + "Name": "given_name", + "Value": "Otto" + }, + { + "Name": "family_name", + "Value": "Readonly" + }, + { + "Name": "email_verified", + "Value": "true" + }, + { + "Name": "custom:state", + "Value": "" + }, + { + "Name": "custom:cms-roles", + "Value": "onemac-micro-readonly" + } + ] + }, { "username": "reviewer@example.com", "attributes": [ @@ -86,6 +144,35 @@ } ] }, + { + "username": "automated-reviewer@example.com", + "attributes": [ + { + "Name": "email", + "Value": "automated-reviewer@example.com" + }, + { + "Name": "given_name", + "Value": "Otto" + }, + { + "Name": "family_name", + "Value": "Reviewer" + }, + { + "Name": "email_verified", + "Value": "true" + }, + { + "Name": "custom:state", + "Value": "" + }, + { + "Name": "custom:cms-roles", + "Value": "onemac-micro-reviewer" + } + ] + }, { "username": "helpdesk@example.com", "attributes": [ @@ -115,6 +202,35 @@ } ] }, + { + "username": "automated-helpdesk@example.com", + "attributes": [ + { + "Name": "email", + "Value": "automated-helpdesk@example.com" + }, + { + "Name": "given_name", + "Value": "Otto" + }, + { + "Name": "family_name", + "Value": "Helpdesk" + }, + { + "Name": "email_verified", + "Value": "true" + }, + { + "Name": "custom:state", + "Value": "" + }, + { + "Name": "custom:cms-roles", + "Value": "onemac-micro-helpdesk" + } + ] + }, { "username": "submitter@example.com", "attributes": [ diff --git a/src/services/data/handlers/index.ts b/src/services/data/handlers/index.ts index c63596f2ba..e2804e89d3 100644 --- a/src/services/data/handlers/index.ts +++ b/src/services/data/handlers/index.ts @@ -2,7 +2,7 @@ import { Handler } from "aws-lambda"; import { send, SUCCESS, FAILED } from "cfn-response-async"; type ResponseStatus = typeof SUCCESS | typeof FAILED; import * as os from "./../../../libs/opensearch-lib"; -import { OsIndex } from "shared-types"; +import { opensearch } from "shared-types"; export const customResourceWrapper: Handler = async (event, context) => { console.log("request:", JSON.stringify(event, undefined, 2)); @@ -26,7 +26,7 @@ export const handler: Handler = async () => { }; const manageIndexResource = async (resource: { - index: OsIndex; + index: opensearch.Index; update?: object; }) => { if (!process.env.osDomain) { @@ -53,12 +53,12 @@ async function manageIndex() { try { await manageIndexResource({ index: "main", + // TODO: remove after rai transform update: { rais: { type: "object", enabled: false } }, }); - await manageIndexResource({ - index: "changelog", - }); + await manageIndexResource({ index: "changelog" }); + await manageIndexResource({ index: "seatool" }); } catch (error) { console.log(error); throw "ERROR: Error occured during index management."; diff --git a/src/services/data/handlers/reindex.ts b/src/services/data/handlers/reindex.ts index 419c0bb2f9..c79b4e04aa 100644 --- a/src/services/data/handlers/reindex.ts +++ b/src/services/data/handlers/reindex.ts @@ -133,6 +133,8 @@ export const deleteIndex: Handler = async () => { throw "process.env.osDomain cannot be undefined"; } await os.deleteIndex(process.env.osDomain, "main"); + await os.deleteIndex(process.env.osDomain, "changelog"); + await os.deleteIndex(process.env.osDomain, "seatool"); } catch (error: any) { if (error.meta.body.error.type == "index_not_found_exception") { console.log("Index does not exist."); diff --git a/src/services/data/handlers/sink.ts b/src/services/data/handlers/sink.ts deleted file mode 100644 index 4992ae377e..0000000000 --- a/src/services/data/handlers/sink.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Handler } from "aws-lambda"; -import { decode } from "base-64"; -import * as os from "./../../../libs/opensearch-lib"; -import { - SeaToolRecordsToDelete, - SeaToolTransform, - transformSeatoolData, - transformOnemac, - transformOnemacLegacy, - transformRaiIssue, - transformRaiResponse, - transformRaiWithdraw, - transformWithdrawPackage, - transformToggleWithdrawRaiEnabled, - Action, -} from "shared-types"; - -type Event = { - /** - * @example "SelfManagedKafka" - */ - eventSource: string; - /** - * @example: "b-1.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094,b-2.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094,b-3.master-msk.zf7e0q.c7.kafka.us-east-1.amazonaws.com:9094" - */ - bootstrapServers: string; // comma separated string - records: Record< - string, - { - topic: string; - partition: number; - offset: number; - timestamp: number; - timestampType: string; - key: string; - headers: string[]; - value: string; - }[] - >; -}; - -if (!process.env.osDomain) { - throw "ERROR: process.env.osDomain is required,"; -} -const osDomain: string = process.env.osDomain; - -export const seatool: Handler = async (event) => { - const seaToolRecords: (SeaToolTransform | SeaToolRecordsToDelete)[] = []; - const docObject: Record = - {}; - const rawArr: any[] = []; - - for (const recordKey of Object.keys(event.records)) { - for (const seatoolRecord of event.records[recordKey] as { - key: string; - value: string; - }[]) { - const { key, value } = seatoolRecord; - - if (value) { - const id: string = JSON.parse(decode(key)); - const record = { id, ...JSON.parse(decode(value)) }; - const validPlanTypeIds = [122, 123, 124, 125]; - const result = transformSeatoolData(id).safeParse(record); - if (!result.success) { - console.log( - "SEATOOL Validation Error. The following record failed to parse: ", - JSON.stringify(record), - "Because of the following Reason(s):", - result.error.message - ); - } else { - if ( - result.data.planTypeId && - validPlanTypeIds.includes(result.data.planTypeId) - ) { - docObject[id] = result.data; - } - rawArr.push(record); - } - } else { - // to handle deletes - const id: string = JSON.parse(decode(key)); - const seaTombstone: SeaToolRecordsToDelete = { - id, - actionType: null, - actionTypeId: null, - approvedEffectiveDate: null, - authority: null, - changedDate: null, - description: null, - finalDispositionDate: null, - leadAnalystName: null, - leadAnalystOfficerId: null, - planType: null, - planTypeId: null, - proposedDate: null, - raiReceivedDate: null, - raiRequestedDate: null, - raiWithdrawnDate: null, - reviewTeam: null, - state: null, - cmsStatus: null, - stateStatus: null, - seatoolStatus: null, - statusDate: null, - submissionDate: null, - subject: null, - }; - - docObject[id] = seaTombstone; - - console.log( - `Record ${id} has been nullified with the following data: `, - JSON.stringify(seaTombstone) - ); - } - } - } - for (const [, b] of Object.entries(docObject)) { - seaToolRecords.push(b); - } - try { - await os.bulkUpdateData(osDomain, "main", seaToolRecords); - await os.bulkUpdateData(osDomain, "seatool", rawArr); - } catch (error) { - console.error(error); - } -}; - -export const onemacDataTransform = (props: { key: string; value?: string }) => { - const id: string = decode(props.key); - - // is delete - if (!props.value) { - return { - id, - additionalInformation: null, - raiWithdrawEnabled: null, - attachments: null, - submitterEmail: null, - submitterName: null, - }; - } - - const record = { id, ...JSON.parse(decode(props.value)) }; - - // is Legacy - if (record?.origin !== "micro") { - if (record?.sk !== "Package") return null; - if (!record.submitterName) return null; - if (record.submitterName === "-- --") return null; - - const result = transformOnemacLegacy(id).safeParse(record); - return result.success ? result.data : null; - } - - // is new create - if (!record?.actionType) { - const result = transformOnemac(id).safeParse(record); - return result.success ? result.data : null; - } - - // --------- Package-Actions ---------// - // TODO: remove transform package-action below - - //ENABLE_RAI_WITHDRAW - if (record.actionType === Action.ENABLE_RAI_WITHDRAW) { - const result = transformToggleWithdrawRaiEnabled(id).safeParse(record); - return result.success ? result.data : null; - } - //DISABLE_RAI_WITHDRAW - if (record.actionType === Action.DISABLE_RAI_WITHDRAW) { - const result = transformToggleWithdrawRaiEnabled(id).safeParse(record); - return result.success ? result.data : null; - } - //ISSUE_RAI - if (record.actionType === Action.ISSUE_RAI) { - const result = transformRaiIssue(id).safeParse(record); - return result.success ? result.data : null; - } - //RESPOND_TO_RAI - if (record.actionType === Action.RESPOND_TO_RAI) { - const result = transformRaiResponse(id).safeParse(record); - return result.success ? result.data : null; - } - //WITHDRAW_RAI - if (record.actionType === Action.WITHDRAW_RAI) { - const result = transformRaiWithdraw(id).safeParse(record); - return result.success ? result.data : null; - } - //WITHDRAW_PACKAGE - if (record.actionType === Action.WITHDRAW_PACKAGE) { - const result = transformWithdrawPackage(id).safeParse(record); - return result.success ? result.data : null; - } - - return null; -}; - -export const onemac_main = async (event: Event) => { - const records = Object.values(event.records).reduce((ACC, RECORDS) => { - RECORDS.forEach((REC) => { - const dataTransform = onemacDataTransform(REC); - if (!dataTransform) return; - ACC.push(dataTransform); - }); - - return ACC; - }, [] as any[]); - - try { - await os.bulkUpdateData(osDomain, "main", records); - } catch (error) { - console.error(error); - } -}; - -export const onemac_changelog = async (event: Event) => { - const data = Object.values(event.records).reduce((ACC, RECORDS) => { - RECORDS.forEach((REC) => { - // omit delete - if (!REC.value) return; - - const record = JSON.parse(decode(REC.value)); - // omit legacy - if (record?.origin !== "micro") return; - - // include package actions - const packageId = decode(REC.key); - ACC.push({ - ...record, - ...(!record?.actionType && { actionType: "new-submission" }), // new-submission custom actionType - id: `${packageId}-${REC.offset}`, - packageId, - }); - }); - - return ACC; - }, [] as any[]); - - try { - await os.bulkUpdateData(osDomain, "changelog", data); - } catch (error) { - console.error(error); - } -}; - -export const onemac: Handler = async (event) => { - await onemac_main(event); - await onemac_changelog(event); -}; diff --git a/src/services/data/handlers/sinkOnemac.ts b/src/services/data/handlers/sinkOnemac.ts new file mode 100644 index 0000000000..d689da3be4 --- /dev/null +++ b/src/services/data/handlers/sinkOnemac.ts @@ -0,0 +1,139 @@ +import { Handler } from "aws-lambda"; +import { decode } from "base-64"; +import * as os from "./../../../libs/opensearch-lib"; +import { opensearch } from "shared-types"; +import { Action } from "shared-types"; +import { KafkaEvent } from "shared-types"; + +const osDomain: string = + process.env.osDomain || + (() => { + throw new Error("ERROR: process.env.osDomain is required"); + })(); + +export const handler: Handler = async (event) => { + await onemac_main(event); + await onemac_changelog(event); +}; + +export const onemac_main = async (event: KafkaEvent) => { + const records = Object.values(event.records).reduce((ACC, RECORDS) => { + RECORDS.forEach((REC) => { + const id: string = decode(REC.key); + + // Handle deletes and return + if (!REC.value) { + ACC.push({ + id, + additionalInformation: null, + raiWithdrawEnabled: null, + attachments: null, + submitterEmail: null, + submitterName: null, + }); + return; + } + + const record = JSON.parse(decode(REC.value)); + + // Handle legacy and return + if (record?.origin !== "micro") { + if ( + record?.sk === "Package" && // Is a Package View + record?.submitterName && // Is originally from Legacy + record?.submitterName !== "-- --" // Is originally from Legacy + ) { + const result = opensearch.main.legacySubmission + .transform(id) + .safeParse(record); + + if (!result.success) { + return console.log( + "LEGACY Validation Error. The following record failed to parse: ", + JSON.stringify(record), + "Because of the following Reason(s):", + result.error.message + ); + } + + ACC.push(result.data); + } + return; + } + + // Handle everything else + const result = (() => { + switch (record?.actionType) { + case undefined: + return opensearch.main.newSubmission + .transform(id) + .safeParse(record); + case Action.DISABLE_RAI_WITHDRAW: + case Action.ENABLE_RAI_WITHDRAW: + return opensearch.main.toggleWithdrawEnabled + .transform(id) + .safeParse(record); + case Action.WITHDRAW_RAI: + return opensearch.main.withdrawRai.transform(id).safeParse(record); + case Action.WITHDRAW_PACKAGE: + return opensearch.main.withdrawPackage + .transform(id) + .safeParse(record); + } + })(); + + if (!result) return; + + if (!result?.success) { + return console.log( + "ONEMAC Validation Error. The following record failed to parse: ", + JSON.stringify(record), + "Because of the following Reason(s):", + result?.error.message + ); + } + + ACC.push(result.data); + }); + + return ACC; + }, [] as any[]); + + try { + await os.bulkUpdateData(osDomain, "main", records); + } catch (error) { + console.error(error); + } +}; + +export const onemac_changelog = async (event: KafkaEvent) => { + const data = Object.values(event.records).reduce((ACC, RECORDS) => { + RECORDS.forEach((REC) => { + // Handle deletes and return + if (!REC.value) return; + + const record = JSON.parse(decode(REC.value)); + + // Handle legacy and return + if (record?.origin !== "micro") return; + + // Handle everything else + const packageId = decode(REC.key); + ACC.push({ + ...record, + ...(!record?.actionType && { actionType: "new-submission" }), // new-submission custom actionType + timestamp: REC.timestamp, + id: `${packageId}-${REC.offset}`, + packageId, + }); + }); + + return ACC; + }, [] as any[]); + + try { + await os.bulkUpdateData(osDomain, "changelog", data); + } catch (error) { + console.error(error); + } +}; diff --git a/src/services/data/handlers/sinkSeatool.ts b/src/services/data/handlers/sinkSeatool.ts new file mode 100644 index 0000000000..35a7869efb --- /dev/null +++ b/src/services/data/handlers/sinkSeatool.ts @@ -0,0 +1,111 @@ +import { Handler } from "aws-lambda"; +import { decode } from "base-64"; +import * as os from "./../../../libs/opensearch-lib"; +import { opensearch } from "shared-types"; +import { KafkaEvent } from "shared-types"; + +const osDomain: string = + process.env.osDomain || + (() => { + throw new Error("ERROR: process.env.osDomain is required"); + })(); + +export const handler: Handler = async (event) => { + await seatool_main(event); + await seatool_seatool(event); +}; + +export const seatool_main = async (event: KafkaEvent) => { + const docs: any[] = []; + const records: any = {}; + + for (const recordKey of Object.keys(event.records)) { + for (const seatoolRecord of event.records[recordKey]) { + const { key, value } = seatoolRecord; + const id: string = JSON.parse(decode(key)); + + // Handle deletes and return + if (!value) { + records[id] = { + id, + actionType: null, + actionTypeId: null, + approvedEffectiveDate: null, + authority: null, + changedDate: null, + description: null, + finalDispositionDate: null, + leadAnalystName: null, + leadAnalystOfficerId: null, + planType: null, + planTypeId: null, + proposedDate: null, + raiReceivedDate: null, + raiRequestedDate: null, + raiWithdrawnDate: null, + reviewTeam: null, + state: null, + cmsStatus: null, + stateStatus: null, + seatoolStatus: null, + statusDate: null, + submissionDate: null, + subject: null, + }; + return; + } + + // Handle everything else + const record = { id, ...JSON.parse(decode(value)) }; + const result = opensearch.main.seatool.transform(id).safeParse(record); + if (!result.success) { + console.log( + "SEATOOL Validation Error. The following record failed to parse: ", + JSON.stringify(record), + "Because of the following Reason(s):", + result.error.message + ); + } else { + const validPlanTypeIds = [122, 123, 124, 125]; + if ( + result.data.planTypeId && + validPlanTypeIds.includes(result.data.planTypeId) + ) { + records[id] = result.data; + } + } + } + } + for (const [, b] of Object.entries(records)) { + docs.push(b); + } + try { + await os.bulkUpdateData(osDomain, "main", docs); + } catch (error) { + console.error(error); + } +}; + +export const seatool_seatool = async (event: KafkaEvent) => { + const data = Object.values(event.records).reduce((ACC, RECORDS) => { + RECORDS.forEach((REC) => { + // omit delete event + if (!REC.value) return; + + const id = decode(REC.key); + const record = JSON.parse(decode(REC.value)); + ACC.push({ + ...record, + id, + }); + }); + + return ACC; + }, [] as any[]); + + try { + await os.bulkUpdateData(osDomain, "seatool", data); + } catch (error) { + console.error(error); + } +}; diff --git a/src/services/data/serverless.yml b/src/services/data/serverless.yml index 4c7aac441e..6bede816ff 100644 --- a/src/services/data/serverless.yml +++ b/src/services/data/serverless.yml @@ -163,7 +163,7 @@ params: functions: sinkSeatool: - handler: handlers/sink.seatool + handler: handlers/sinkSeatool.handler environment: region: ${self:provider.region} osDomain: !Sub https://${OpenSearch.DomainEndpoint} @@ -176,7 +176,7 @@ functions: subnetIds: >- ${self:custom.vpc.privateSubnets} sinkOnemac: - handler: handlers/sink.onemac + handler: handlers/sinkOnemac.handler environment: region: ${self:provider.region} osDomain: !Sub https://${OpenSearch.DomainEndpoint} diff --git a/src/services/email/.eslintrc.js b/src/services/email/.eslintrc.js new file mode 100644 index 0000000000..daf3ecee97 --- /dev/null +++ b/src/services/email/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + root: true, + extends: ["custom-server"], + }; + \ No newline at end of file diff --git a/src/services/email/handlers/processEmailEvents.js b/src/services/email/handlers/processEmailEvents.js new file mode 100644 index 0000000000..954a2d8748 --- /dev/null +++ b/src/services/email/handlers/processEmailEvents.js @@ -0,0 +1,15 @@ +export const main = async (event, context, callback) => { + console.log( + "Received email event, stringified:", + JSON.stringify(event, null, 4) + ); + + let message; + if (typeof event.Records[0].Sns.Message === "string") + message = { "simpleMessage": event.Records[0].Sns.Message }; + else + message = JSON.parse(event.Records[0].Sns.Message); + console.log("Message received from SNS:", message); + + callback(null, "Success"); +}; diff --git a/src/services/email/handlers/processEmails.js b/src/services/email/handlers/processEmails.js new file mode 100644 index 0000000000..9d4dc33402 --- /dev/null +++ b/src/services/email/handlers/processEmails.js @@ -0,0 +1,44 @@ +import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; + +const createSendEmailCommand = (event) => + new SendEmailCommand({ + Source: "kgrue@fearless.tech", + Destination: { + ToAddresses: [ + "k.grue.stateuser@gmail.com", + ], + }, + Message: { + Subject: { + Data: event.subject ?? "Subject Required", + Charset: "UTF-8", + }, + Body: { + Text: { + Data: "Body Text", + Charset: "UTF-8", + }, + Html: { + Data: "

HTML body text

yup

", + Charset: "UTF-8", + }, + }, + }, + ConfigurationSetName: process.env.emailConfigSet, + }); + +const SES = new SESClient({ region: process.env.region }); + +export const main = async (event, context, callback) => { + let response; + console.log("Received event (stringified):", JSON.stringify(event, null, 4)); + const sendEmailCommand = createSendEmailCommand(event); + + try { + response = await SES.send(sendEmailCommand); + console.log("sendEmailCommand response: ", response); + } catch (err) { + console.log("Failed to process emails.", err); + } + callback(null, "Success"); +}; diff --git a/src/services/email/package.json b/src/services/email/package.json new file mode 100644 index 0000000000..8035c136ea --- /dev/null +++ b/src/services/email/package.json @@ -0,0 +1,21 @@ +{ + "name": "email", + "description": "", + "private": true, + "version": "0.0.0", + "main": "index.js", + "author": "", + "license": "CC0-1.0", + "dependencies": { + "@aws-sdk/client-ses": "^3.499.0" + }, + "scripts": { + "lint": "eslint '**/*.{ts,js}'" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2" + } +} diff --git a/src/services/email/serverless.yml b/src/services/email/serverless.yml new file mode 100644 index 0000000000..2af508dcae --- /dev/null +++ b/src/services/email/serverless.yml @@ -0,0 +1,171 @@ +service: ${self:custom.project}-email + +frameworkVersion: "3" + +plugins: + - serverless-stack-termination-protection + - "@stratiformdigital/serverless-s3-security-helper" + - "@stratiformdigital/serverless-iam-helper" + - serverless-plugin-scripts + - serverless-esbuild + +provider: + name: aws + runtime: nodejs18.x + region: us-east-1 + iam: + role: + path: /delegatedadmin/developer/ + permissionsBoundary: arn:aws:iam::${aws:accountId}:policy/cms-cloud-admin/developer-boundary-policy + statements: + - Effect: Allow + Action: + - sts:AssumeRole + Resource: "*" + - Effect: Allow + Action: + - ses:ListIdentities + - ses:ListConfigurationSets + - ses:SendEmail + Resource: "*" + - Effect: Allow + Action: + - sns:Subscribe + - sns:Publish + Resource: "*" + + stackTags: + PROJECT: ${self:custom.project} + SERVICE: ${self:service} + +custom: + project: ${env:PROJECT} + emailEventTopicName: ${self:service}-${sls:stage}-email-events + serverlessTerminationProtection: + stages: # Apply CloudFormation termination protection for these stages + - master + - val + - production + +functions: + processEmails: + handler: handlers/processEmails.main + environment: + region: ${self:provider.region} + emailConfigSet: ${self:service}-${sls:stage}-configuration + maximumRetryAttempts: 0 + timeout: 60 + memorySize: 1024 + processEmailEvents: + handler: handlers/processEmailEvents.main + events: + - sns: + arn: !Ref EmailEventTopic + topicName: ${self:custom.emailEventTopicName} + +resources: + Resources: + EmailEventTopic: + Type: AWS::SNS::Topic + Properties: + TopicName: ${self:custom.emailEventTopicName} + DisplayName: Monitoring the sending of emails + KmsMasterKeyId: !Ref KmsKeyForEmails + + KmsKeyForEmails: + Type: AWS::KMS::Key + Properties: + EnableKeyRotation: "true" + KeyPolicy: + Version: "2012-10-17" + Statement: + - Sid: Allow access for Root User + Effect: Allow + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: "kms:*" + Resource: "*" + - Sid: Allow access for Key User (SNS Service Principal) + Effect: Allow + Principal: + Service: "sns.amazonaws.com" + Action: + - "kms:GenerateDataKey" + - "kms:Decrypt" + Resource: "*" + - Sid: Allow CloudWatch events to use the key + Effect: Allow + Principal: + Service: events.amazonaws.com + Action: + - "kms:Decrypt" + - "kms:GenerateDataKey" + Resource: "*" + - Sid: Allow CloudWatch for CMK + Effect: Allow + Principal: + Service: + - cloudwatch.amazonaws.com + Action: + - "kms:Decrypt" + - "kms:GenerateDataKey*" + Resource: "*" + - Sid: Allow SES events to use the key + Effect: Allow + Principal: + Service: + - ses.amazonaws.com + Action: + - "kms:Decrypt" + - "kms:GenerateDataKey*" + Resource: "*" + + EmailEventTopicPolicy: + Type: AWS::SNS::TopicPolicy + Properties: + PolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + - ses.amazonaws.com + Action: + - sns:Subscribe + - sns:Publish + Resource: !Ref EmailEventTopic + Topics: + - !Ref EmailEventTopic + + EmailEventSubscription: + Type: AWS::SNS::Subscription + Properties: + TopicArn: !Ref EmailEventTopic + Endpoint: !GetAtt ProcessEmailEventsLambdaFunction.Arn + Protocol: lambda + + EmailEventConfigurationSet: + Type: AWS::SES::ConfigurationSet + Properties: + Name: "${self:service}-${sls:stage}-configuration" + + EmailEventConfigurationSetEventDestination: + Type: AWS::SES::ConfigurationSetEventDestination + Properties: + ConfigurationSetName: !Ref EmailEventConfigurationSet + EventDestination: + Enabled: true + Name: "${self:service}-${sls:stage}-destination" + MatchingEventTypes: + - "send" + - "reject" + - "bounce" + - "complaint" + - "delivery" + - "open" + - "click" + - "renderingFailure" + - "deliveryDelay" + - "subscription" + SnsDestination: + TopicARN: !Ref EmailEventTopic diff --git a/src/services/ui/e2e/pages/index.ts b/src/services/ui/e2e/pages/index.ts new file mode 100644 index 0000000000..b311de19c0 --- /dev/null +++ b/src/services/ui/e2e/pages/index.ts @@ -0,0 +1 @@ +export * from "./loginPage"; diff --git a/src/services/ui/e2e/pages/loginPage.ts b/src/services/ui/e2e/pages/loginPage.ts new file mode 100644 index 0000000000..13c740bb5d --- /dev/null +++ b/src/services/ui/e2e/pages/loginPage.ts @@ -0,0 +1,21 @@ +import { Page, expect } from "@playwright/test"; + +export class LoginPage { + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto("/"); + await this.page.getByRole("button", { name: "Sign In" }).click(); + } + + async login(email: string, password: string) { + await this.goto(); + await this.page.getByRole("textbox", { name: "name@host.com" }).fill(email); + await this.page.getByRole("textbox", { name: "Password" }).fill(password); + await this.page.getByRole("button", { name: "submit" }).click(); + await this.page.waitForLoadState("networkidle"); + expect( + await this.page.getByRole("link", { name: "Dashboard" }) + ).toBeVisible(); + } +} diff --git a/src/services/ui/e2e/tests/a11y/index.spec.ts b/src/services/ui/e2e/tests/a11y/index.spec.ts new file mode 100644 index 0000000000..c1a38edd42 --- /dev/null +++ b/src/services/ui/e2e/tests/a11y/index.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; + +const staticRoutes = [ + "/", + "/dashboard", + "/faq", + "/profile", + "/new-submission", + "/new-submission/spa", + "/new-submission/spa/medicaid", + "/new-submission/spa/chip", + "/new-submission/waiver", + "/new-submission/waiver/b", + "/new-submission/waiver/b/b4", + "/new-submission/waiver/b/capitated", + "/new-submission/spa/medicaid/landing/medicaid-abp", + "/new-submission/spa/medicaid/landing/medicaid-eligibility", + "/new-submission/spa/chip/landing/chip-eligibility", +]; + +test.describe("test a11y on static routes", () => { + for (const route of staticRoutes) { + test(`${route} should not have any automatically detectable accessibility issues`, async ({ + page, + }) => { + await page.goto(route); + await page.waitForLoadState("networkidle"); // playwright is so fast this is sometimes helpful to slow it down to view results + const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); + console.log( + `${route} violations: `, + accessibilityScanResults.violations.length + ); + expect(accessibilityScanResults.violations).toEqual([]); + }); + } +}); + +const webformRoutes = [ + "/webforms", + "/guides/abp", + "/webform/abp10/1", + "/webform/abp3_1/1", + "/webform/abp3/1", + "/webform/abp1/1", +]; + +test.describe("test a11y on webform routes", () => { + for (const route of webformRoutes) { + test(`${route} should not have any automatically detectable accessibility issues`, async ({ + page, + }) => { + await page.goto(route); + await page.waitForLoadState("networkidle"); + const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); + console.log( + `${route} violations: `, + accessibilityScanResults.violations.length + ); + expect(accessibilityScanResults.violations).toEqual([]); + }); + } +}); diff --git a/src/services/ui/e2e/tests/home/index.spec.ts b/src/services/ui/e2e/tests/home/index.spec.ts index e4e7616ff1..4757a72aa5 100644 --- a/src/services/ui/e2e/tests/home/index.spec.ts +++ b/src/services/ui/e2e/tests/home/index.spec.ts @@ -1,16 +1,4 @@ -import * as Libs from "../../../../../libs/secrets-manager-lib"; import { test, expect } from "@playwright/test"; -import { testUsers } from "e2e/utils/users"; -const stage = - process.env.STAGE_NAME === "production" || process.env.STAGE_NAME === "val" - ? process.env.STAGE_NAME - : "default"; -const secretId = `${process.env.PROJECT}/${stage}/bootstrapUsersPassword`; - -const password = (await Libs.getSecretsValue( - process.env.REGION_A as string, - secretId -)) as string; test("has title", async ({ page }) => { await page.goto("/"); @@ -32,12 +20,6 @@ test("see frequently asked questions header when in faq page", async ({ test("see dashboard link when log in", async ({ page }) => { await page.goto("/"); - await page.getByRole("button", { name: "Sign In" }).click(); - await page - .getByRole("textbox", { name: "name@host.com" }) - .fill(testUsers.state); - await page.getByRole("textbox", { name: "Password" }).fill(password); - await page.getByRole("button", { name: "submit" }).click(); await page.getByRole("link", { name: "Dashboard" }).click(); const dashboardLinkVisible = await page @@ -45,12 +27,3 @@ test("see dashboard link when log in", async ({ page }) => { .isVisible(); expect(dashboardLinkVisible).toBeTruthy(); }); - -test("failed incorrect login username", async ({ page }) => { - await page.goto("/"); - await page.getByRole("button", { name: "Sign In" }).click(); - await page.getByRole("textbox", { name: "name@host.com" }).fill("."); - await page.getByRole("textbox", { name: "Password" }).fill(password); - await page.getByRole("button", { name: "submit" }).click(); - await page.locator("#loginErrorMessage").first().isVisible(); -}); diff --git a/src/services/ui/e2e/utils/auth.setup.ts b/src/services/ui/e2e/utils/auth.setup.ts new file mode 100644 index 0000000000..cfcf5be90e --- /dev/null +++ b/src/services/ui/e2e/utils/auth.setup.ts @@ -0,0 +1,35 @@ +import { test as setup } from "@playwright/test"; +import * as Libs from "../../../../libs/secrets-manager-lib"; +import { testUsers } from "./users"; +import { LoginPage } from "../pages"; + +const stage = + process.env.STAGE_NAME === "production" || process.env.STAGE_NAME === "val" + ? process.env.STAGE_NAME + : "default"; +const secretId = `${process.env.PROJECT}/${stage}/bootstrapUsersPassword`; + +const password = (await Libs.getSecretsValue( + process.env.REGION_A as string, + secretId +)) as string; + +const stateSubmitterAuthFile = "playwright/.auth/state-user.json"; + +setup("authenticate state submitter", async ({ page, context }) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + + await loginPage.login(testUsers.state, password); + await context.storageState({ path: stateSubmitterAuthFile }); +}); + +const reviewerAuthFile = "playwright/.auth/reviewer-user.json"; + +setup("authenticate cms reviewer", async ({ page, context }) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + + await loginPage.login(testUsers.reviewer, password); + await context.storageState({ path: reviewerAuthFile }); +}); diff --git a/src/services/ui/e2e/utils/users.ts b/src/services/ui/e2e/utils/users.ts index d80170240d..1076ba94d1 100644 --- a/src/services/ui/e2e/utils/users.ts +++ b/src/services/ui/e2e/utils/users.ts @@ -1,4 +1,4 @@ export const testUsers = { - state: "george@example.com", - cmsAdmin: "cmsadmin@example.com" -}; \ No newline at end of file + state: "george@example.com", + reviewer: "reviewer@example.com", +}; diff --git a/src/services/ui/package.json b/src/services/ui/package.json index b581ebc2a7..e736f46680 100644 --- a/src/services/ui/package.json +++ b/src/services/ui/package.json @@ -55,6 +55,7 @@ "jszip": "^3.10.1", "lucide-react": "^0.291.0", "lz-string": "^1.5.0", + "moment-timezone": "^0.5.44", "react": "^18.2.0", "react-day-picker": "^8.8.1", "react-dom": "^18.2.0", @@ -71,6 +72,7 @@ "zod": "^3.22.3" }, "devDependencies": { + "@axe-core/playwright": "^4.8.3", "@playwright/test": "^1.38.0", "@tailwindcss/typography": "^0.5.10", "@testing-library/jest-dom": "^5.16.5", @@ -95,6 +97,7 @@ "tailwindcss": "^3.3.1", "typescript": "^5.2.0", "vite": "^4.2.0", + "vite-plugin-radar": "^0.9.2", "vitest": "^0.30.1" } } diff --git a/src/services/ui/playwright.config.ts b/src/services/ui/playwright.config.ts index 61159b6c7d..3ffb614799 100644 --- a/src/services/ui/playwright.config.ts +++ b/src/services/ui/playwright.config.ts @@ -33,18 +33,19 @@ export default defineConfig({ /* Configure projects for major browsers */ // Note: we can test on multiple browsers and resolutions defined here projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, + // Setup project + { name: "setup", testMatch: /.*\.setup\.ts/, fullyParallel: true }, { - name: "webkit", - use: { ...devices["Desktop Safari"] }, + // we can have different projects for different users/use cases + name: "logged in state user", + use: { + ...devices["Desktop Chrome"], + // Use prepared auth state for state submitter. + storageState: "playwright/.auth/state-user.json", + }, + // Tests start already authenticated because we specified storageState in the config. + dependencies: ["setup"], }, ], }); diff --git a/src/services/ui/public/forms/abp/ABPSPAProcess.pdf b/src/services/ui/public/forms/abp/ABPSPAProcess.pdf new file mode 100644 index 0000000000..a55bc3c5aa Binary files /dev/null and b/src/services/ui/public/forms/abp/ABPSPAProcess.pdf differ diff --git a/src/services/ui/public/forms/abp/ABPStateTraining.pdf b/src/services/ui/public/forms/abp/ABPStateTraining.pdf new file mode 100644 index 0000000000..f22c78f831 Binary files /dev/null and b/src/services/ui/public/forms/abp/ABPStateTraining.pdf differ diff --git a/src/services/ui/public/forms/abp/IG_ABP10_GeneralAssurances.doc b/src/services/ui/public/forms/abp/IG_ABP10_GeneralAssurances.doc new file mode 100644 index 0000000000..8ba9be2f93 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP10_GeneralAssurances.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP11_PaymentMethodology.doc b/src/services/ui/public/forms/abp/IG_ABP11_PaymentMethodology.doc new file mode 100644 index 0000000000..f5ab2734b2 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP11_PaymentMethodology.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP1_AlternativeBenefitPlanPopulations.doc b/src/services/ui/public/forms/abp/IG_ABP1_AlternativeBenefitPlanPopulations.doc new file mode 100644 index 0000000000..1de8070111 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP1_AlternativeBenefitPlanPopulations.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP2a_VoluntaryBenefitPackageAssurances.doc b/src/services/ui/public/forms/abp/IG_ABP2a_VoluntaryBenefitPackageAssurances.doc new file mode 100644 index 0000000000..c1affd052c Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP2a_VoluntaryBenefitPackageAssurances.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP2b_VoluntaryEnrollmentAssurances.doc b/src/services/ui/public/forms/abp/IG_ABP2b_VoluntaryEnrollmentAssurances.doc new file mode 100644 index 0000000000..6f1a4b0dd9 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP2b_VoluntaryEnrollmentAssurances.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP2c_EnrollmentAssurancesMandatoryParticipants.doc b/src/services/ui/public/forms/abp/IG_ABP2c_EnrollmentAssurancesMandatoryParticipants.doc new file mode 100644 index 0000000000..74fd9ff5fc Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP2c_EnrollmentAssurancesMandatoryParticipants.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP3.1_SelectionOfBenchmark20190819-Final.docx b/src/services/ui/public/forms/abp/IG_ABP3.1_SelectionOfBenchmark20190819-Final.docx new file mode 100644 index 0000000000..6d32f8257f Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP3.1_SelectionOfBenchmark20190819-Final.docx differ diff --git a/src/services/ui/public/forms/abp/IG_ABP3_SelectionOfBenchmark20190819-Final.docx b/src/services/ui/public/forms/abp/IG_ABP3_SelectionOfBenchmark20190819-Final.docx new file mode 100644 index 0000000000..ccd4cd0f0b Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP3_SelectionOfBenchmark20190819-Final.docx differ diff --git a/src/services/ui/public/forms/abp/IG_ABP4_AbpCostSharing.doc b/src/services/ui/public/forms/abp/IG_ABP4_AbpCostSharing.doc new file mode 100644 index 0000000000..be593602ca Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP4_AbpCostSharing.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP5_BenefitsDescription-Final.docx b/src/services/ui/public/forms/abp/IG_ABP5_BenefitsDescription-Final.docx new file mode 100644 index 0000000000..e8f8860990 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP5_BenefitsDescription-Final.docx differ diff --git a/src/services/ui/public/forms/abp/IG_ABP6_BenchmarkEquivalentBenefit.doc b/src/services/ui/public/forms/abp/IG_ABP6_BenchmarkEquivalentBenefit.doc new file mode 100644 index 0000000000..8f55d95c8c Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP6_BenchmarkEquivalentBenefit.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP7_BenefitAssurances.doc b/src/services/ui/public/forms/abp/IG_ABP7_BenefitAssurances.doc new file mode 100644 index 0000000000..9435ca96c8 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP7_BenefitAssurances.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP8_ServiceDeliverySystems.doc b/src/services/ui/public/forms/abp/IG_ABP8_ServiceDeliverySystems.doc new file mode 100644 index 0000000000..6b19d479f6 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP8_ServiceDeliverySystems.doc differ diff --git a/src/services/ui/public/forms/abp/IG_ABP9_EmployerSponsoredInsurance.doc b/src/services/ui/public/forms/abp/IG_ABP9_EmployerSponsoredInsurance.doc new file mode 100644 index 0000000000..610df91b82 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_ABP9_EmployerSponsoredInsurance.doc differ diff --git a/src/services/ui/public/forms/abp/IG_AbpIntroduction.doc b/src/services/ui/public/forms/abp/IG_AbpIntroduction.doc new file mode 100644 index 0000000000..e85659c417 Binary files /dev/null and b/src/services/ui/public/forms/abp/IG_AbpIntroduction.doc differ diff --git a/src/services/ui/serverless.yml b/src/services/ui/serverless.yml index 44b945f32c..6c7263f0f0 100644 --- a/src/services/ui/serverless.yml +++ b/src/services/ui/serverless.yml @@ -21,6 +21,7 @@ custom: stage: ${opt:stage, self:provider.stage} region: ${opt:region, self:provider.region} idmInfo: ${ssm:/aws/reference/secretsmanager/${self:custom.project}/${sls:stage}/idmInfo, ""} + googleAnalytics: ${ssm:/aws/reference/secretsmanager/${self:custom.project}/${sls:stage}/googleAnalytics, ssm:/aws/reference/secretsmanager/${self:custom.project}/default/googleAnalytics} serverlessTerminationProtection: stages: - master @@ -46,6 +47,8 @@ custom: VITE_COGNITO_REDIRECT_SIGNIN=${param:ApplicationEndpointUrl} VITE_COGNITO_REDIRECT_SIGNOUT=${param:ApplicationEndpointUrl} VITE_IDM_HOME_URL=${self:custom.idmInfo.home_url, "https://test.home.idm.cms.gov"} + VITE_GOOGLE_ANALYTICS_GTAG=${self:custom.googleAnalytics.gtag} + VITE_GOOGLE_ANALYTICS_DISABLE=${self:custom.googleAnalytics.disable, "false"} """ > .env.local yarn build deploy:finalize: | diff --git a/src/services/ui/src/api/submissionService.ts b/src/services/ui/src/api/submissionService.ts index 331896e7b8..4971dcccbd 100644 --- a/src/services/ui/src/api/submissionService.ts +++ b/src/services/ui/src/api/submissionService.ts @@ -1,6 +1,6 @@ import { API } from "aws-amplify"; import { - OnemacAttachmentSchema, + Attachment, PlanType, ReactQueryApiError, Action, @@ -9,6 +9,7 @@ import { import { buildActionUrl, SubmissionServiceEndpoint } from "@/lib"; import { OneMacUser } from "@/api/useGetUser"; import { useMutation, UseMutationOptions } from "@tanstack/react-query"; +import { seaToolFriendlyTimestamp } from "shared-utils"; type SubmissionServiceParameters = { data: T; @@ -34,9 +35,7 @@ type UploadRecipe = PreSignedURL & { /** Pass in an array of UploadRecipes and get a back-end compatible object * to store attachment data */ -const buildAttachmentObject = ( - recipes: UploadRecipe[] -): OnemacAttachmentSchema[] => { +const buildAttachmentObject = (recipes: UploadRecipe[]): Attachment[] => { return recipes .map( (r) => @@ -46,7 +45,7 @@ const buildAttachmentObject = ( title: r.title, bucket: r.bucket, uploadDate: Date.now(), - } as OnemacAttachmentSchema) + } as Attachment) ) .flat(); }; @@ -65,8 +64,6 @@ const buildSubmissionPayload = >( submitterName: `${user?.user?.given_name} ${user?.user?.family_name}` ?? "N/A", }; - const seaToolFriendlyTimestamp = - Math.floor(new Date().getTime() / 1000) * 1000; // Truncating to match seatool switch (endpoint) { case "/submit": @@ -75,7 +72,9 @@ const buildSubmissionPayload = >( origin: "micro", ...data, ...userDetails, - proposedEffectiveDate: (data.proposedEffectiveDate as Date).getTime(), + proposedEffectiveDate: seaToolFriendlyTimestamp( + data.proposedEffectiveDate as Date + ), attachments: attachments ? buildAttachmentObject(attachments) : null, state: (data.id as string).split("-")[0], }; @@ -86,7 +85,6 @@ const buildSubmissionPayload = >( ...data, ...userDetails, attachments: attachments ? buildAttachmentObject(attachments) : null, - withdrawnDate: seaToolFriendlyTimestamp, }; case buildActionUrl(Action.ISSUE_RAI): return { @@ -94,7 +92,6 @@ const buildSubmissionPayload = >( origin: "micro", ...data, ...userDetails, - requestedDate: seaToolFriendlyTimestamp, attachments: attachments ? buildAttachmentObject(attachments) : null, }; case buildActionUrl(Action.RESPOND_TO_RAI): @@ -103,7 +100,6 @@ const buildSubmissionPayload = >( origin: "micro", ...data, ...userDetails, - responseDate: seaToolFriendlyTimestamp, attachments: attachments ? buildAttachmentObject(attachments) : null, }; case buildActionUrl(Action.WITHDRAW_PACKAGE): diff --git a/src/services/ui/src/api/useGetItem.ts b/src/services/ui/src/api/useGetItem.ts index 12371aeae5..0d65819f93 100644 --- a/src/services/ui/src/api/useGetItem.ts +++ b/src/services/ui/src/api/useGetItem.ts @@ -1,8 +1,10 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { API } from "aws-amplify"; -import { ItemResult, ReactQueryApiError } from "shared-types"; +import { opensearch, ReactQueryApiError } from "shared-types"; -export const getItem = async (id: string): Promise => +export const getItem = async ( + id: string +): Promise => await API.post("os", "/item", { body: { id } }); export const idIsUnique = async (id: string) => { @@ -16,9 +18,9 @@ export const idIsUnique = async (id: string) => { export const useGetItem = ( id: string, - options?: UseQueryOptions + options?: UseQueryOptions ) => { - return useQuery( + return useQuery( ["record", id], () => getItem(id), options diff --git a/src/services/ui/src/api/useSearch.ts b/src/services/ui/src/api/useSearch.ts index 5e5f9504b9..992268d490 100644 --- a/src/services/ui/src/api/useSearch.ts +++ b/src/services/ui/src/api/useSearch.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { aggQueryBuilder, filterQueryBuilder, @@ -6,26 +7,23 @@ import { } from "@/components/Opensearch/utils"; import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { API } from "aws-amplify"; -import type { - OsQueryState, - ReactQueryApiError, - OsFilterable, - OsAggQuery, - OsMainSearchResponse, - OsMainSourceItem, -} from "shared-types"; +import type { ReactQueryApiError, opensearch } from "shared-types"; -type QueryProps = { - filters: OsQueryState["filters"]; - sort?: OsQueryState["sort"]; - pagination: OsQueryState["pagination"]; - aggs?: OsAggQuery[]; +type QueryProps = { + index: opensearch.Index; + filters: opensearch.QueryState["filters"]; + sort?: opensearch.QueryState["sort"]; + pagination: opensearch.QueryState["pagination"]; + aggs?: opensearch.AggQuery[]; }; -export const getSearchData = async ( - props: QueryProps -): Promise => { - const searchData = await API.post("os", "/search", { +export const getOsData = async < + TProps, + TResponse extends opensearch.Response +>( + props: QueryProps +): Promise => { + const searchData = await API.post("os", `/search/${props.index}`, { body: { ...filterQueryBuilder(props.filters), ...paginationQueryBuilder(props.pagination), @@ -38,17 +36,19 @@ export const getSearchData = async ( return searchData; }; -export const getAllSearchData = async (filters?: OsFilterable[]) => { +export const getMainExportData = async ( + filters?: opensearch.main.Filterable[] +) => { if (!filters) return []; const recursiveSearch = async ( startPage: number - ): Promise => { + ): Promise => { if (startPage * 1000 >= 10000) { return []; } - const searchData = await API.post("os", "/search", { + const searchData = await API.post("os", "/search/main", { body: { ...filterQueryBuilder(filters), ...paginationQueryBuilder({ number: startPage, size: 1000 }), @@ -69,15 +69,30 @@ export const getAllSearchData = async (filters?: OsFilterable[]) => { return await recursiveSearch(0); }; -export const useOsSearch = ( +export const useOsSearch = ( options?: UseMutationOptions< - OsMainSearchResponse, + TResponse, ReactQueryApiError, - QueryProps + QueryProps > ) => { - return useMutation( - (props) => getSearchData(props), + //@ts-ignore + return useMutation>( + (props) => getOsData(props), options ); }; + +export const useChangelogSearch = ( + options?: UseMutationOptions< + opensearch.changelog.Response, + ReactQueryApiError, + QueryProps + > +) => { + return useMutation< + opensearch.changelog.Response, + ReactQueryApiError, + QueryProps + >((props) => getOsData(props), options); +}; diff --git a/src/services/ui/src/components/AdditionalInfo/index.tsx b/src/services/ui/src/components/AdditionalInfo/index.tsx index 5c7600a9f2..e27edc3871 100644 --- a/src/services/ui/src/components/AdditionalInfo/index.tsx +++ b/src/services/ui/src/components/AdditionalInfo/index.tsx @@ -1,9 +1,9 @@ -import { OsMainSourceItem } from "shared-types"; +import { opensearch } from "shared-types"; export const AdditionalInfo = ({ additionalInformation, }: { - additionalInformation: OsMainSourceItem["additionalInformation"]; + additionalInformation: opensearch.main.Document["additionalInformation"]; }) => { return (
diff --git a/src/services/ui/src/components/AttachmentsList/index.tsx b/src/services/ui/src/components/AttachmentsList/index.tsx index ffd2d9c837..b5afd69640 100644 --- a/src/services/ui/src/components/AttachmentsList/index.tsx +++ b/src/services/ui/src/components/AttachmentsList/index.tsx @@ -4,7 +4,7 @@ import { BLANK_VALUE } from "@/consts"; import { DownloadIcon } from "lucide-react"; import JSZip from "jszip"; import { saveAs } from "file-saver"; -import { OsMainSourceItem } from "shared-types"; +import { opensearch } from "shared-types"; import { useState } from "react"; import { Button } from "../Inputs/button"; import { @@ -122,7 +122,7 @@ export const Attachmentslist = (data: AttachmentList) => { }; async function downloadAll( - attachments: OsMainSourceItem["attachments"], + attachments: opensearch.main.Document["attachments"], id: string ) { if (!attachments) return null; diff --git a/src/services/ui/src/components/DetailsSection/index.tsx b/src/services/ui/src/components/DetailsSection/index.tsx index c3e1ccf9f1..c3f22d0a88 100644 --- a/src/services/ui/src/components/DetailsSection/index.tsx +++ b/src/services/ui/src/components/DetailsSection/index.tsx @@ -1,6 +1,6 @@ interface DetailsSectionProps { children: React.ReactNode; - title: string; + title: React.ReactNode; id: string; description?: string; } @@ -12,8 +12,10 @@ export const DetailsSection: React.FC = ({ id, }: DetailsSectionProps) => { return ( -
+

{title}

+
+ {description &&

{description}

} {children} diff --git a/src/services/ui/src/components/ExportButton/index.tsx b/src/services/ui/src/components/ExportButton/index.tsx index 1a0a30b497..9465a2aeaa 100644 --- a/src/services/ui/src/components/ExportButton/index.tsx +++ b/src/services/ui/src/components/ExportButton/index.tsx @@ -4,12 +4,12 @@ import { Download, Loader } from "lucide-react"; import { useState } from "react"; import { motion } from "framer-motion"; import { format } from "date-fns"; -import { useOsUrl } from "../Opensearch"; -import { OsExportHeaderOptions } from "shared-types"; +import { useOsUrl } from "@/components/Opensearch/main"; +import { opensearch } from "shared-types"; type Props> = { data: TData[] | (() => Promise); - headers: OsExportHeaderOptions[]; + headers: opensearch.ExportHeaderOptions[]; // | Record> }; diff --git a/src/services/ui/src/components/HowItWorks/index.tsx b/src/services/ui/src/components/HowItWorks/index.tsx index bec8e16992..e323cc4eef 100644 --- a/src/services/ui/src/components/HowItWorks/index.tsx +++ b/src/services/ui/src/components/HowItWorks/index.tsx @@ -1,7 +1,7 @@ export const HowItWorks = ({ children }: React.PropsWithChildren) => { return (
-

How it works

+

How it works

{children}
); diff --git a/src/services/ui/src/components/Inputs/button.tsx b/src/services/ui/src/components/Inputs/button.tsx index e69a796fff..4fe2ca9d72 100644 --- a/src/services/ui/src/components/Inputs/button.tsx +++ b/src/services/ui/src/components/Inputs/button.tsx @@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; +import { Loader2 } from "lucide-react"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", @@ -38,17 +39,26 @@ export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; + loading?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ( + { className, variant, size, loading, children, asChild = false, ...props }, + ref + ) => { const Comp = asChild ? Slot : "button"; return ( + > + <> + {loading && } + {children} + + ); } ); diff --git a/src/services/ui/src/components/Inputs/checkbox.tsx b/src/services/ui/src/components/Inputs/checkbox.tsx index 9fc81980c7..1124f2c2de 100644 --- a/src/services/ui/src/components/Inputs/checkbox.tsx +++ b/src/services/ui/src/components/Inputs/checkbox.tsx @@ -1,48 +1,61 @@ import * as React from "react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import { Check } from "lucide-react"; - +import { DependencyWrapper } from "../RHF/dependencyWrapper"; import { cn } from "@/lib/utils"; - +import { DependencyWrapperProps } from "shared-types"; const Checkbox = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - className?: string; - label: string; - description?: string; - } ->(({ className, ...props }, ref) => ( -
- & + DependencyWrapperProps & { + className?: string; + label: string; + value?: string; + description?: string; + } +>(({ className, ...props }, ref) => { + const { changeMethod, parentValue, ...rest } = props; + + return ( + - - - - -
- {!!props.label && ( - - )} - {!!props.description && ( -

{props.description}

- )} -
-
-)); + + + + +
+ {!!props.label && ( + + )} + {!!props.description && ( +

{props.description}

+ )} +
+
+ + ); +}); Checkbox.displayName = CheckboxPrimitive.Root.displayName; export const CheckboxGroup: React.FC<{ diff --git a/src/services/ui/src/components/Inputs/upload.tsx b/src/services/ui/src/components/Inputs/upload.tsx index ae3da73bbb..2c1f824ce8 100644 --- a/src/services/ui/src/components/Inputs/upload.tsx +++ b/src/services/ui/src/components/Inputs/upload.tsx @@ -51,7 +51,7 @@ export const Upload = ({ maxFiles, files, setFiles }: UploadProps) => { className="flex border-2 rounded-md py-1 pl-2.5 pr-1 border-sky-500 items-center" key={file.name} > - {file.name} + {file.name} { e.preventDefault(); @@ -60,7 +60,7 @@ export const Upload = ({ maxFiles, files, setFiles }: UploadProps) => { variant="ghost" className="p-0 h-0" > - + {/*
*/}
@@ -76,7 +76,7 @@ export const Upload = ({ maxFiles, files, setFiles }: UploadProps) => { >

Drag file here or{" "} - + choose from folder

diff --git a/src/services/ui/src/components/Layout/index.tsx b/src/services/ui/src/components/Layout/index.tsx index 899c9cc2b9..4779de9389 100644 --- a/src/services/ui/src/components/Layout/index.tsx +++ b/src/services/ui/src/components/Layout/index.tsx @@ -8,11 +8,11 @@ import { Auth } from "aws-amplify"; import { AwsCognitoOAuthOpts } from "@aws-amplify/auth/lib-esm/types"; import { Footer } from "../Footer"; import { UsaBanner } from "../UsaBanner"; -import { FAQ_TARGET } from "@/routes"; import { useUserContext } from "../Context/userContext"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import config from "@/config"; import { useNavigate } from "../Routing"; +import { FAQ_TAB } from "../Routing/consts"; const getLinks = (isAuthenticated: boolean, role?: boolean) => { const isProd = window && window.location.hostname === "mako.cms.gov"; @@ -57,7 +57,7 @@ const UserDropdownMenu = () => { asChild className="hover:text-white/70 p-4 data-[state=open]:bg-white data-[state=open]:text-primary" > -
+
+ { return (
-
+
+
@@ -182,7 +182,7 @@ const ResponsiveNav = ({ isDesktop }: ResponsiveNavProps) => { {getLinks(!!data.user, role).map((link) => ( @@ -227,7 +227,7 @@ const ResponsiveNav = ({ isDesktop }: ResponsiveNavProps) => { {link.name} diff --git a/src/services/ui/src/components/Opensearch/Filtering/consts.ts b/src/services/ui/src/components/Opensearch/Filtering/consts.ts deleted file mode 100644 index b0f92db23c..0000000000 --- a/src/services/ui/src/components/Opensearch/Filtering/consts.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - OsExportHeaderOptions, - OsField, - OsFilterable, - OsMainSourceItem, -} from "shared-types"; -import { OsFilterComponentType, OsTab } from "../types"; -import { UserRoles } from "shared-types"; -import { BLANK_VALUE } from "@/consts"; -import { LABELS } from "@/lib/labels"; -import { format } from "date-fns"; - -type DrawerFilterableGroup = { - label: string; - component: OsFilterComponentType; -}; -type FilterGroup = Partial< - Record ->; - -const SPA_FILTER_GROUP = (isCms: boolean): FilterGroup => { - return { - "state.keyword": { - label: "State", - field: "state.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - "planType.keyword": { - label: "Type", - field: "planType.keyword", - component: "multiCheck", - prefix: "must", - type: "terms", - value: [], - }, - [isCms ? "cmsStatus.keyword" : "stateStatus.keyword"]: { - label: "Status", - field: isCms ? "cmsStatus.keyword" : "stateStatus.keyword", - component: "multiCheck", - prefix: "must", - type: "terms", - value: [], - }, - submissionDate: { - label: "Initial Submission", - field: "submissionDate", - component: "dateRange", - prefix: "must", - type: "range", - value: { gte: undefined, lte: undefined }, - }, - raiReceivedDate: { - label: "Formal RAI Response", - field: "raiReceivedDate", - component: "dateRange", - prefix: "must", - type: "range", - value: { gte: undefined, lte: undefined }, - }, - "leadAnalystName.keyword": { - label: "CPOC Name", - field: "leadAnalystName.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - "origin.keyword": { - label: "Submission Source", - field: "origin.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - }; -}; - -const WAIVER_FILTER_GROUP = (isCms: boolean): FilterGroup => { - return { - "state.keyword": { - label: "State", - field: "state.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - "planType.keyword": { - label: "Type", - field: "planType.keyword", - component: "multiCheck", - prefix: "must", - type: "terms", - value: [], - }, - "actionType.keyword": { - label: "Action Type", - field: "actionType.keyword", - component: "multiCheck", - prefix: "must", - type: "terms", - value: [], - }, - [isCms ? "cmsStatus.keyword" : "stateStatus.keyword"]: { - label: "Status", - field: isCms ? "cmsStatus.keyword" : "stateStatus.keyword", - component: "multiCheck", - prefix: "must", - type: "terms", - value: [], - }, - submissionDate: { - label: "Initial Submission", - field: "submissionDate", - component: "dateRange", - prefix: "must", - type: "range", - value: { gte: undefined, lte: undefined }, - }, - raiReceivedDate: { - label: "Formal RAI Response", - field: "raiReceivedDate", - component: "dateRange", - prefix: "must", - type: "range", - value: { gte: undefined, lte: undefined }, - }, - "leadAnalystName.keyword": { - label: "CPOC Name", - field: "leadAnalystName.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - "origin.keyword": { - label: "Submission Source", - field: "origin.keyword", - component: "multiSelect", - prefix: "must", - type: "terms", - value: [], - }, - }; -}; - -/** - * @desc - * - label: ui label - * - field: opensearch field property (should match key) - * - component: filterable component type - * - prefix: query prefix - * - type: query type - * - value: query value - */ -export const FILTER_GROUPS = (user?: any, tab?: OsTab): FilterGroup => { - const isCms = - !!user?.isCms && - !user.user?.["custom:cms-roles"].includes(UserRoles.HELPDESK); - - switch (tab) { - case "spas": - return SPA_FILTER_GROUP(isCms); - case "waivers": - default: - return WAIVER_FILTER_GROUP(isCms); - } -}; - -// Export headers dependent on tab -export const EXPORT_GROUPS = ( - tab: OsTab, - user?: any -): OsExportHeaderOptions[] => { - const idFieldName = - tab === "spas" ? "SPA ID" : tab === "waivers" ? "Waiver Number" : ""; - const actionField: OsExportHeaderOptions[] = - tab === "waivers" - ? [ - { - name: "Action Type", - transform: (data) => { - if (data.actionType === undefined) { - return BLANK_VALUE; - } - - return ( - LABELS[data.actionType as keyof typeof LABELS] || - data.actionType - ); - }, - }, - ] - : []; - - return [ - { - name: idFieldName, - transform: (data) => data.id, - }, - { - name: "State", - transform: (data) => data.state ?? BLANK_VALUE, - }, - { - name: "Type", - transform: (data) => data.planType ?? BLANK_VALUE, - }, - ...actionField, - { - name: "Status", - transform(data) { - if (user?.data?.isCms && !user?.data?.user) { - if (data.cmsStatus) { - return data.cmsStatus; - } - return BLANK_VALUE; - } else { - if (data.stateStatus) { - return data.stateStatus; - } - return BLANK_VALUE; - } - }, - }, - { - name: "Initial Submission", - transform: (data) => - data?.submissionDate - ? format(new Date(data.submissionDate), "MM/dd/yyyy") - : BLANK_VALUE, - }, - { - name: "Formal RAI Response", - transform: (data) => { - return data.raiReceivedDate && !data.raiWithdrawnDate - ? format(new Date(data.raiReceivedDate), "MM/dd/yyyy") - : BLANK_VALUE; - }, - }, - { - name: "CPOC Name", - transform: (data) => data.leadAnalystName ?? BLANK_VALUE, - }, - { - name: "Submitted By", - transform: (data) => data.submitterName ?? BLANK_VALUE, - }, - ]; -}; diff --git a/src/services/ui/src/components/Opensearch/Filtering/useFilterDrawer.ts b/src/services/ui/src/components/Opensearch/Filtering/useFilterDrawer.ts deleted file mode 100644 index 296d4bea1b..0000000000 --- a/src/services/ui/src/components/Opensearch/Filtering/useFilterDrawer.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { useState, useEffect, useMemo } from "react"; -import type { OsField } from "../types"; -import * as Consts from "./consts"; -import { useOsAggregate, useOsUrl } from "../useOpensearch"; -import { OsFilterValue, OsRangeValue } from "shared-types"; -import { useLabelMapping } from "@/hooks"; -import { useFilterDrawerContext } from "./FilterProvider"; -import { useGetUser } from "@/api/useGetUser"; - -export const useFilterDrawer = () => { - const { drawerOpen, setDrawerState } = useFilterDrawerContext(); - const { data: user } = useGetUser(); - const url = useOsUrl(); - const [filters, setFilters] = useState( - Consts.FILTER_GROUPS(user, url.state.tab) - ); - const [accordionValues, setAccordionValues] = useState([]); - const labelMap = useLabelMapping(); - const _aggs = useOsAggregate(); - - const onFilterChange = (field: OsField) => { - return (value: OsFilterValue) => { - setFilters((state) => { - const updateState = { ...state, [field]: { ...state[field], value } }; - const updateFilters = Object.values(updateState).filter((FIL) => { - if (FIL.type === "terms") { - const value = FIL.value as string[]; - return value?.length; - } - - if (FIL.type === "range") { - const value = FIL.value as OsRangeValue; - return !!value?.gte && !!value?.lte; - } - - return true; - }); - - url.onSet((state) => ({ - ...state, - filters: updateFilters, - pagination: { ...state.pagination, number: 0 }, - })); - - return updateState; - }); - }; - }; - - const onAccordionChange = (updateAccordion: string[]) => { - setAccordionValues(updateAccordion); - }; - - // update initial filter state + accordion default open items - useEffect(() => { - if (drawerOpen) return; - const updateAccordions = [] as any[]; - - setFilters((s) => { - return Object.entries(s).reduce((STATE, [KEY, VAL]) => { - const updateFilter = url.state.filters.find((FIL) => FIL.field === KEY); - - const value = (() => { - if (updateFilter) { - updateAccordions.push(KEY); - return updateFilter.value; - } - if (VAL.type === "terms") return [] as string[]; - return { gte: undefined, lte: undefined } as OsRangeValue; - })(); - - STATE[KEY] = { ...VAL, value }; - - return STATE; - }, {} as any); - }); - setAccordionValues(updateAccordions); - }, [url.state.filters, drawerOpen]); - - // change base filters per tab - useEffect(() => { - setFilters(Consts.FILTER_GROUPS(user, url.state.tab)); - }, [url.state.tab]); - - const aggs = useMemo(() => { - return Object.entries(_aggs || {}).reduce((STATE, [KEY, AGG]) => { - return { - ...STATE, - [KEY]: AGG.buckets.map((BUCK) => ({ - label: `${labelMap[BUCK.key] || BUCK.key}`, - value: BUCK.key, - })), - }; - }, {} as Record); - }, [_aggs]); - - return { - aggs, - drawerOpen, - accordionValues, - filters, - onFilterChange, - setDrawerState, - onAccordionChange, - }; -}; diff --git a/src/services/ui/src/components/Opensearch/changelog/index.ts b/src/services/ui/src/components/Opensearch/changelog/index.ts new file mode 100644 index 0000000000..c50c39f0c2 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/changelog/index.ts @@ -0,0 +1 @@ +export const _ = "filler "; diff --git a/src/services/ui/src/components/Opensearch/index.ts b/src/services/ui/src/components/Opensearch/index.ts index aecfc92713..178cd64f81 100644 --- a/src/services/ui/src/components/Opensearch/index.ts +++ b/src/services/ui/src/components/Opensearch/index.ts @@ -1,5 +1 @@ -export * from "./useOpensearch"; -export * from "./types"; -export * from "./Table"; -export * from "./Filtering"; -export * from "./Provider"; +export * from "./utils"; diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterChips.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Chipbar/index.tsx similarity index 60% rename from src/services/ui/src/components/Opensearch/Filtering/FilterChips.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/Chipbar/index.tsx index c24f768079..d0f8788beb 100644 --- a/src/services/ui/src/components/Opensearch/Filtering/FilterChips.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Chipbar/index.tsx @@ -1,30 +1,45 @@ -import { type FC, useCallback, Fragment } from "react"; +import { type FC, useCallback } from "react"; import { Chip } from "@/components/Chip"; -import { useOsUrl } from "@/components/Opensearch"; -import { OsFilterable, OsRangeValue } from "shared-types"; -import { useFilterDrawerContext } from "./FilterProvider"; -import { checkMultiFilter, resetFilters } from "../utils"; +import { useOsUrl } from "@/components/Opensearch/main"; +import { opensearch } from "shared-types"; +import { useFilterDrawerContext } from "../FilterProvider"; +import { checkMultiFilter } from "@/components/Opensearch"; import { useLabelMapping } from "@/hooks"; -interface RenderProp { - filter: OsFilterable; +export interface RenderProp { + filter: opensearch.main.Filterable; index: number; openDrawer: () => void; - clearFilter: (filter: OsFilterable, valIndex?: number) => void; + clearFilter: (filter: opensearch.main.Filterable, valIndex?: number) => void; } -// simple date range chips -const DateChip: FC = ({ +export const ChipBool: FC = ({ filter, - index, openDrawer, clearFilter, }) => { - const value = filter.value as OsRangeValue; + const value = filter.value as opensearch.RangeValue; + return ( + { + clearFilter(filter); + }} + > + {filter?.label}: {value ? "Yes" : "No"} + + ); +}; + +export const ChipDate: FC = ({ + filter, + openDrawer, + clearFilter, +}) => { + const value = filter.value as opensearch.RangeValue; return ( { clearFilter(filter); @@ -39,10 +54,8 @@ const DateChip: FC = ({ ); }; -// array value chips -const ChipList: FC = ({ +export const ChipTerms: FC = ({ filter, - index, clearFilter, openDrawer, }) => { @@ -51,12 +64,12 @@ const ChipList: FC = ({ if (!Array.isArray(filter.value)) return null; return ( - + <> {filter.value.map((v, vindex) => { const chipText = `${filter?.label + ": " ?? ""}${labelMap[v] ?? v}`; return ( { clearFilter(filter, vindex); @@ -66,7 +79,7 @@ const ChipList: FC = ({ ); })} - + ); }; @@ -76,7 +89,10 @@ export const FilterChips: FC = () => { const openDrawer = useCallback(() => setDrawerState(true), [setDrawerState]); const twoOrMoreFiltersApplied = checkMultiFilter(url.state.filters, 2); - const clearFilter = (filter: OsFilterable, valIndex?: number) => { + const clearFilter = ( + filter: opensearch.main.Filterable, + valIndex?: number + ) => { url.onSet((s) => { let filters = s.filters; const filterIndex = filters.findIndex((f) => f.field === filter.field); @@ -98,14 +114,23 @@ export const FilterChips: FC = () => { }); }; - const handleChipClick = () => resetFilters(url.onSet); + const handleChipClick = () => + url.onSet((s) => ({ + ...s, + filters: [], + pagination: { ...s.pagination, number: 0 }, + })); return (
{url.state.filters.map((filter, index) => { const props: RenderProp = { clearFilter, openDrawer, filter, index }; - if (filter.type === "range") return ; - if (filter.type === "terms") return ; + const key = `${filter.field}-${index}`; + + if (filter.type === "range") return ; + if (filter.type === "terms") return ; + if (filter.type === "match") return ; + return null; })} {twoOrMoreFiltersApplied && ( diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Boolean.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Boolean.tsx new file mode 100644 index 0000000000..9ff0e7e86e --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Boolean.tsx @@ -0,0 +1,25 @@ +import { Checkbox } from "@/components/Inputs"; +import { FC } from "react"; + +export const FilterableBoolean: FC<{ + value: boolean | null; + onChange: (val: boolean | null) => void; +}> = (props) => { + const yes = props.value === null ? false : props.value; + const no = props.value === null ? false : !props.value; + + const onYes = (mhm: boolean) => { + props.onChange(mhm ? true : null); + }; + + const onNo = (mhm: boolean) => { + props.onChange(mhm ? false : null); + }; + + return ( +
+ + +
+ ); +}; diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterableDateRange.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx similarity index 96% rename from src/services/ui/src/components/Opensearch/Filtering/FilterableDateRange.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx index 7d624208f6..9aa3e61775 100644 --- a/src/services/ui/src/components/Opensearch/Filtering/FilterableDateRange.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/DateRange.tsx @@ -18,14 +18,14 @@ import { DateRange } from "react-day-picker"; import { cn } from "@/lib/utils"; import { Button, Calendar, Input } from "@/components/Inputs"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/Popover"; -import { OsRangeValue } from "shared-types"; +import { opensearch } from "shared-types"; type Props = Omit< React.HTMLAttributes, "onChange" | "value" | "onSelect" > & { - value: OsRangeValue; - onChange: (val: OsRangeValue) => void; + value: opensearch.RangeValue; + onChange: (val: opensearch.RangeValue) => void; className?: string; }; @@ -113,7 +113,10 @@ export function FilterableDateRange({ value, onChange, ...props }: Props) { } }; - const getDateRange = (startDate: Date, endDate: Date): OsRangeValue => { + const getDateRange = ( + startDate: Date, + endDate: Date + ): opensearch.RangeValue => { return { gte: startDate.toISOString(), lte: endDate.toISOString(), diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterableCheckbox.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Multicheck.tsx similarity index 91% rename from src/services/ui/src/components/Opensearch/Filtering/FilterableCheckbox.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Multicheck.tsx index d7b9352f41..4f66754e43 100644 --- a/src/services/ui/src/components/Opensearch/Filtering/FilterableCheckbox.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Multicheck.tsx @@ -6,7 +6,7 @@ type Props = { options: { label: string; value: string }[]; }; -export const FilterableCheckbox = (props: Props) => { +export const FilterableMultiCheck = (props: Props) => { const onClear = () => { props.onChange([]); }; diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterableSelect.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Select.tsx similarity index 100% rename from src/services/ui/src/components/Opensearch/Filtering/FilterableSelect.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/Select.tsx diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/index.ts b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/index.ts new file mode 100644 index 0000000000..289c0acbdc --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/Filterable/index.ts @@ -0,0 +1,4 @@ +export * from "./Boolean"; +export * from "./DateRange"; +export * from "./Multicheck"; +export * from "./Select"; diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/consts.ts b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/consts.ts new file mode 100644 index 0000000000..20da50fa85 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/consts.ts @@ -0,0 +1,106 @@ +import { opensearch } from "shared-types"; +import { OsFilterComponentType } from "../../types"; + +/** + * @desc + * - label: ui label + * - field: opensearch field property (should match key) + * - component: filterable component type + * - prefix: query prefix + * - type: query type + * - value: query value + */ +export type DrawerFilterableGroup = { + label: string; + component: OsFilterComponentType; +} & opensearch.main.Filterable; + +export const SELECT_STATE: DrawerFilterableGroup = { + label: "State", + field: "state.keyword", + component: "multiSelect", + prefix: "must", + type: "terms", + value: [], +}; + +export const CHECK_PLANTYPE: DrawerFilterableGroup = { + label: "Type", + field: "planType.keyword", + component: "multiCheck", + prefix: "must", + type: "terms", + value: [], +}; + +export const CHECK_CMSSTATUS: DrawerFilterableGroup = { + label: "Status", + field: "cmsStatus.keyword", + component: "multiCheck", + prefix: "must", + type: "terms", + value: [], +}; + +export const CHECK_STATESTATUS: DrawerFilterableGroup = { + label: "Status", + field: "stateStatus.keyword", + component: "multiCheck", + prefix: "must", + type: "terms", + value: [], +}; + +export const BOOL_INITIALINTAKENEEDED: DrawerFilterableGroup = { + label: "Initial Intake Needed", + field: "initialIntakeNeeded", + component: "boolean", + prefix: "must", + type: "match", + value: null, +}; + +export const CHECK_ACTIONTYPE: DrawerFilterableGroup = { + label: "Action Type", + field: "actionType.keyword", + component: "multiCheck", + prefix: "must", + type: "terms", + value: [], +}; + +export const DATE_SUBMISSION: DrawerFilterableGroup = { + label: "Initial Submission", + field: "submissionDate", + component: "dateRange", + prefix: "must", + type: "range", + value: { gte: undefined, lte: undefined }, +}; + +export const DATE_RAIRECEIVED: DrawerFilterableGroup = { + label: "Formal RAI Response", + field: "raiReceivedDate", + component: "dateRange", + prefix: "must", + type: "range", + value: { gte: undefined, lte: undefined }, +}; + +export const SELECT_CPOC: DrawerFilterableGroup = { + label: "CPOC Name", + field: "leadAnalystName.keyword", + component: "multiSelect", + prefix: "must", + type: "terms", + value: [], +}; + +export const SELECT_ORIGIN: DrawerFilterableGroup = { + label: "Submission Source", + field: "origin.keyword", + component: "multiSelect", + prefix: "must", + type: "terms", + value: [], +}; diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/hooks.ts b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/hooks.ts new file mode 100644 index 0000000000..c72097a8c3 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/hooks.ts @@ -0,0 +1,173 @@ +import { useState, useEffect, useMemo } from "react"; +import { useGetUser } from "@/api/useGetUser"; +import { UserRoles } from "shared-types"; + +import * as C from "./consts"; +import { useOsAggregate, useOsUrl } from "../../useOpensearch"; +import { opensearch } from "shared-types"; +import { useLabelMapping } from "@/hooks"; +import { useFilterDrawerContext } from "../FilterProvider"; +import { checkMultiFilter } from "@/components/Opensearch"; + +type FilterGroup = Partial< + Record +>; + +export const useFilterState = () => { + const { data: user } = useGetUser(); + const url = useOsUrl(); + + const isCms = + !!user?.isCms && + !user.user?.["custom:cms-roles"].includes(UserRoles.HELPDESK); + + const filters: FilterGroup = (() => { + // ------------------------ SPAS ------------------------ // + if (url.state.tab === "spas") { + return { + [C.SELECT_STATE.field]: C.SELECT_STATE, + [C.CHECK_PLANTYPE.field]: C.CHECK_PLANTYPE, + ...(() => { + if (isCms) return { [C.CHECK_CMSSTATUS.field]: C.CHECK_CMSSTATUS }; + return { [C.CHECK_STATESTATUS.field]: C.CHECK_STATESTATUS }; + })(), + ...(!!user?.isCms && { + [C.BOOL_INITIALINTAKENEEDED.field]: C.BOOL_INITIALINTAKENEEDED, + }), + [C.DATE_SUBMISSION.field]: C.DATE_SUBMISSION, + [C.DATE_RAIRECEIVED.field]: C.DATE_RAIRECEIVED, + [C.SELECT_CPOC.field]: C.SELECT_CPOC, + [C.SELECT_ORIGIN.field]: C.SELECT_ORIGIN, + }; + } + + // ------------------------ WAIVERS ------------------------ // + if (url.state.tab === "waivers") { + return { + [C.SELECT_STATE.field]: C.SELECT_STATE, + [C.CHECK_PLANTYPE.field]: C.CHECK_PLANTYPE, + [C.CHECK_ACTIONTYPE.field]: C.CHECK_ACTIONTYPE, + ...(() => { + if (isCms) return { [C.CHECK_CMSSTATUS.field]: C.CHECK_CMSSTATUS }; + return { [C.CHECK_STATESTATUS.field]: C.CHECK_STATESTATUS }; + })(), + ...(!!user?.isCms && { + [C.BOOL_INITIALINTAKENEEDED.field]: C.BOOL_INITIALINTAKENEEDED, + }), + [C.DATE_SUBMISSION.field]: C.DATE_SUBMISSION, + [C.DATE_RAIRECEIVED.field]: C.DATE_RAIRECEIVED, + [C.SELECT_CPOC.field]: C.SELECT_CPOC, + [C.SELECT_ORIGIN.field]: C.SELECT_ORIGIN, + }; + } + + return {}; + })(); + + return useState(filters); +}; + +export const useFilterDrawer = () => { + const url = useOsUrl(); + const drawer = useFilterDrawerContext(); + const [filters, setFilters] = useFilterState(); + + const [accordionValues, setAccordionValues] = useState([]); + const labelMap = useLabelMapping(); + const _aggs = useOsAggregate(); + + const onFilterChange = (field: opensearch.main.Field) => { + return (value: opensearch.FilterValue) => { + setFilters((state) => { + const updateState = { ...state, [field]: { ...state[field], value } }; + const updateFilters = Object.values(updateState).filter((FIL) => { + if (FIL.type === "terms") { + const value = FIL.value as string[]; + return value?.length; + } + + if (FIL.type === "range") { + const value = FIL.value as opensearch.RangeValue; + return !!value?.gte && !!value?.lte; + } + + if (FIL.type === "match") { + if (FIL.value === null) return false; + } + + return true; + }); + + url.onSet((state) => ({ + ...state, + filters: updateFilters, + pagination: { ...state.pagination, number: 0 }, + })); + + return updateState; + }); + }; + }; + + const onAccordionChange = (updateAccordion: string[]) => { + setAccordionValues(updateAccordion); + }; + + const onFilterReset = () => + url.onSet((s) => ({ + ...s, + filters: [], + pagination: { ...s.pagination, number: 0 }, + })); + + const filtersApplied = checkMultiFilter(url.state.filters, 1); + + // update initial filter state + accordion default open items + useEffect(() => { + if (!drawer.drawerOpen) return; + const updateAccordions = [...accordionValues] as any[]; + + setFilters((s) => { + return Object.entries(s).reduce((STATE, [KEY, VAL]) => { + const updateFilter = url.state.filters.find((FIL) => FIL.field === KEY); + + const value = (() => { + if (updateFilter) { + updateAccordions.push(KEY); + return updateFilter.value; + } + if (VAL.type === "terms") return [] as string[]; + if (VAL.type === "match") return null; + return { gte: undefined, lte: undefined } as opensearch.RangeValue; + })(); + + STATE[KEY] = { ...VAL, value }; + return STATE; + }, {} as any); + }); + setAccordionValues(updateAccordions); + }, [url.state.filters, drawer.drawerOpen]); + + const aggs = useMemo(() => { + return Object.entries(_aggs || {}).reduce((STATE, [KEY, AGG]) => { + return { + ...STATE, + [KEY]: AGG.buckets.map((BUCK) => ({ + label: `${labelMap[BUCK.key] || BUCK.key}`, + value: BUCK.key, + })), + }; + }, {} as Record); + }, [_aggs]); + + return { + aggs, + drawer, + accordionValues, + filters, + filtersApplied, + onFilterReset, + onFilterChange, + onAccordionChange, + }; +}; diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterDrawer.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/index.tsx similarity index 71% rename from src/services/ui/src/components/Opensearch/Filtering/FilterDrawer.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/Drawer/index.tsx index 59afaf7f08..22e09b2c49 100644 --- a/src/services/ui/src/components/Opensearch/Filtering/FilterDrawer.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Drawer/index.tsx @@ -1,5 +1,5 @@ import { FilterIcon } from "lucide-react"; -import { OsRangeValue } from "shared-types"; +import { opensearch } from "shared-types"; import { Sheet, @@ -13,23 +13,19 @@ import { AccordionItem, AccordionTrigger, } from "@/components/Accordion"; - -import { FilterableSelect } from "./FilterableSelect"; -import { FilterableDateRange } from "./FilterableDateRange"; -import { FilterableCheckbox } from "./FilterableCheckbox"; -import { useFilterDrawer } from "./useFilterDrawer"; import { Button } from "@/components/Inputs"; -import { checkMultiFilter, resetFilters } from "../utils"; -import { useOsUrl } from "../useOpensearch"; + +import * as F from "./Filterable"; +import { useFilterDrawer } from "./hooks"; export const OsFilterDrawer = () => { const hook = useFilterDrawer(); - const url = useOsUrl(); - const filtersApplied = checkMultiFilter(url.state.filters, 1); - const handleFilterReset = () => resetFilters(url.onSet); return ( - + @@ -63,25 +59,35 @@ export const OsFilterDrawer = () => { {PK.component === "multiSelect" && ( - )} {PK.component === "multiCheck" && ( - )} {PK.component === "dateRange" && ( - )} + {PK.component === "boolean" && ( + <> + + + )} ))} diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Export/hooks.ts b/src/services/ui/src/components/Opensearch/main/Filtering/Export/hooks.ts new file mode 100644 index 0000000000..1ef49207db --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Export/hooks.ts @@ -0,0 +1,103 @@ +import { useGetUser } from "@/api/useGetUser"; +import { UserRoles } from "shared-types"; +import { DEFAULT_FILTERS, useOsUrl } from "../../useOpensearch"; +import { opensearch } from "shared-types"; +import { LABELS } from "@/lib/labels"; +import { BLANK_VALUE } from "@/consts"; +import { formatSeatoolDate } from "shared-utils"; +import { getMainExportData } from "@/api"; + +export const useFilterExportGroups = () => { + const { data: user } = useGetUser(); + const url = useOsUrl(); + + const onExport = () => + getMainExportData( + url.state.filters.concat(DEFAULT_FILTERS[url.state.tab]?.filters ?? []) + ); + + const headers: opensearch.main.ExportHeader[] = [ + { + name: (() => { + if (url.state.tab === "spas") return "SPA ID"; + if (url.state.tab === "waivers") return "Waiver Number"; + return ""; + })(), + transform: (data) => data.id, + }, + { + name: "State", + transform: (data) => data.state ?? BLANK_VALUE, + }, + { + name: "Type", + transform: (data) => data.planType ?? BLANK_VALUE, + }, + ...((): opensearch.main.ExportHeader[] => { + if (url.state.tab !== "waivers") return []; + return [ + { + name: "Action Type", + transform: (data) => { + if (data.actionType === undefined) { + return BLANK_VALUE; + } + + return ( + LABELS[data.actionType as keyof typeof LABELS] || data.actionType + ); + }, + }, + ]; + })(), + { + name: "Status", + transform: (data) => { + const status = (() => { + if (!user?.isCms) return data.stateStatus; + if (user?.user?.["custom:cms-roles"].includes(UserRoles.HELPDESK)) { + return data.stateStatus; + } + return data.cmsStatus; + })(); + + const subStatusRAI = data.raiWithdrawEnabled + ? " (Withdraw Formal RAI Response - Enabled)" + : ""; + + const subStatusInitialIntake = (() => { + if (!user?.isCms) return ""; + if (!data.initialIntakeNeeded) return ""; + return " (Initial Intake Needed)"; + })(); + + return `${status}${subStatusRAI}${subStatusInitialIntake}`; + }, + }, + { + name: "Initial Submission", + transform: (data) => + data?.submissionDate + ? formatSeatoolDate(data.submissionDate) + : BLANK_VALUE, + }, + { + name: "Formal RAI Response", + transform: (data) => { + return data.raiReceivedDate && !data.raiWithdrawnDate + ? formatSeatoolDate(data.raiReceivedDate) + : BLANK_VALUE; + }, + }, + { + name: "CPOC Name", + transform: (data) => data.leadAnalystName ?? BLANK_VALUE, + }, + { + name: "Submitted By", + transform: (data) => data.submitterName ?? BLANK_VALUE, + }, + ]; + + return { onExport, headers }; +}; diff --git a/src/services/ui/src/components/Opensearch/main/Filtering/Export/index.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/Export/index.tsx new file mode 100644 index 0000000000..a69a883922 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/Filtering/Export/index.tsx @@ -0,0 +1,8 @@ +import { ExportButton } from "@/components/ExportButton"; +import { useFilterExportGroups } from "./hooks"; + +export const OsFilterExport = () => { + const hook = useFilterExportGroups(); + + return ; +}; diff --git a/src/services/ui/src/components/Opensearch/Filtering/FilterProvider.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/FilterProvider.tsx similarity index 100% rename from src/services/ui/src/components/Opensearch/Filtering/FilterProvider.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/FilterProvider.tsx diff --git a/src/services/ui/src/components/Opensearch/Filtering/index.tsx b/src/services/ui/src/components/Opensearch/main/Filtering/index.tsx similarity index 57% rename from src/services/ui/src/components/Opensearch/Filtering/index.tsx rename to src/services/ui/src/components/Opensearch/main/Filtering/index.tsx index c0a88b43cb..3cff055cd4 100644 --- a/src/services/ui/src/components/Opensearch/Filtering/index.tsx +++ b/src/services/ui/src/components/Opensearch/main/Filtering/index.tsx @@ -1,24 +1,19 @@ import { SearchForm } from "@/components"; import { FC } from "react"; -import { DEFAULT_FILTERS, useOsUrl } from "../useOpensearch"; -import { ExportButton } from "@/components/ExportButton"; +import { useOsUrl } from "../useOpensearch"; import { useOsContext } from "../Provider"; -import { OsFilterDrawer } from "./FilterDrawer"; -import { getAllSearchData } from "@/api"; -import { useGetUser } from "@/api/useGetUser"; -import { EXPORT_GROUPS } from "./consts"; +import { OsFilterDrawer } from "./Drawer"; +import { OsFilterExport } from "./Export"; export const OsFiltering: FC<{ disabled?: boolean; }> = (props) => { const url = useOsUrl(); const context = useOsContext(); - const user = useGetUser(); - const filters = DEFAULT_FILTERS[url.state.tab]?.filters ?? []; return (
-

+

{"Search by Package ID, CPOC Name, or Submitter Name"}

@@ -34,10 +29,7 @@ export const OsFiltering: FC<{ disabled={!!props.disabled} />
- getAllSearchData([...url.state.filters, ...filters])} - headers={EXPORT_GROUPS(url.state.tab, user)} - /> +
@@ -45,5 +37,5 @@ export const OsFiltering: FC<{ ); }; -export * from "./FilterChips"; +export * from "./Chipbar"; export * from "./FilterProvider"; diff --git a/src/services/ui/src/components/Opensearch/Provider/index.tsx b/src/services/ui/src/components/Opensearch/main/Provider/index.tsx similarity index 80% rename from src/services/ui/src/components/Opensearch/Provider/index.tsx rename to src/services/ui/src/components/Opensearch/main/Provider/index.tsx index b89793547c..750b00d57d 100644 --- a/src/services/ui/src/components/Opensearch/Provider/index.tsx +++ b/src/services/ui/src/components/Opensearch/main/Provider/index.tsx @@ -1,9 +1,9 @@ import { ReactNode } from "react"; import { createContextProvider } from "@/utils"; -import { ReactQueryApiError, SearchData } from "shared-types"; +import { ReactQueryApiError, opensearch } from "shared-types"; type ContextState = { - data: SearchData | undefined; + data: opensearch.main.Response["hits"] | undefined; isLoading: boolean; error: ReactQueryApiError | null; }; diff --git a/src/services/ui/src/components/Opensearch/Settings/Visibility.tsx b/src/services/ui/src/components/Opensearch/main/Settings/Visibility.tsx similarity index 100% rename from src/services/ui/src/components/Opensearch/Settings/Visibility.tsx rename to src/services/ui/src/components/Opensearch/main/Settings/Visibility.tsx diff --git a/src/services/ui/src/components/Opensearch/Settings/index.ts b/src/services/ui/src/components/Opensearch/main/Settings/index.ts similarity index 100% rename from src/services/ui/src/components/Opensearch/Settings/index.ts rename to src/services/ui/src/components/Opensearch/main/Settings/index.ts diff --git a/src/services/ui/src/components/Opensearch/Table/index.tsx b/src/services/ui/src/components/Opensearch/main/Table/index.tsx similarity index 85% rename from src/services/ui/src/components/Opensearch/Table/index.tsx rename to src/services/ui/src/components/Opensearch/main/Table/index.tsx index 1b614d5668..0b8ed11445 100644 --- a/src/services/ui/src/components/Opensearch/Table/index.tsx +++ b/src/services/ui/src/components/Opensearch/main/Table/index.tsx @@ -3,10 +3,10 @@ import { LoadingSpinner } from "@/components/LoadingSpinner"; import { FC, useState } from "react"; import { OsTableColumn } from "./types"; import { useOsContext } from "../Provider"; -import { useOsUrl } from "../useOpensearch"; +import { useOsUrl } from "@/components/Opensearch/main"; import { VisibilityPopover } from "../Settings"; import { BLANK_VALUE } from "@/consts"; -import { OsField } from "shared-types"; +import { opensearch } from "shared-types"; export const OsTable: FC<{ columns: OsTableColumn[]; @@ -33,8 +33,8 @@ export const OsTable: FC<{ }; return ( - - + + ({ ...s, sort: { - field: TH.field as OsField, + field: TH.field as opensearch.main.Field, order: s.sort.order === "desc" ? "asc" : "desc", }, })); @@ -76,13 +76,15 @@ export const OsTable: FC<{ {/* TODO: Add a skeleton loader after discussing with HCD. See https://qmacbis.atlassian.net/browse/OY2-25623 */} {!context.data && ( -
- -
+ + + + + )} {context.data && !context.data.hits.length && ( - +

No Results Found @@ -91,9 +93,9 @@ export const OsTable: FC<{ )} {context.data?.hits.map((DAT) => ( - + - {osColumns.map((COL, IDX) => { + {osColumns.map((COL) => { if (COL.hidden) return null; return ( ["_source"]) => ReactNode; + cell: (data: opensearch.main.Document) => ReactNode; }; diff --git a/src/services/ui/src/components/Opensearch/main/index.ts b/src/services/ui/src/components/Opensearch/main/index.ts new file mode 100644 index 0000000000..aecfc92713 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/index.ts @@ -0,0 +1,5 @@ +export * from "./useOpensearch"; +export * from "./types"; +export * from "./Table"; +export * from "./Filtering"; +export * from "./Provider"; diff --git a/src/services/ui/src/components/Opensearch/main/types.ts b/src/services/ui/src/components/Opensearch/main/types.ts new file mode 100644 index 0000000000..bc0332ccc8 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/main/types.ts @@ -0,0 +1,17 @@ +import type { opensearch } from "shared-types"; + +export type OsDrawerFilterable = opensearch.main.Filterable & { open: boolean }; +export type OsFilterComponentType = + | "multiSelect" + | "multiCheck" + | "dateRange" + | "boolean"; +export type OsFilterGroup = { + label: string; + field: opensearch.main.Field; + type: OsFilterComponentType; +}; + +export type OsTab = "waivers" | "spas"; + +export * from "./Table/types"; diff --git a/src/services/ui/src/components/Opensearch/useOpensearch.ts b/src/services/ui/src/components/Opensearch/main/useOpensearch.ts similarity index 85% rename from src/services/ui/src/components/Opensearch/useOpensearch.ts rename to src/services/ui/src/components/Opensearch/main/useOpensearch.ts index f0560115f6..804de56434 100644 --- a/src/services/ui/src/components/Opensearch/useOpensearch.ts +++ b/src/services/ui/src/components/Opensearch/main/useOpensearch.ts @@ -1,8 +1,8 @@ -import { getSearchData, useOsSearch } from "@/api"; +import { getOsData, useOsSearch } from "@/api"; import { useLzUrl } from "@/hooks/useParams"; import { useEffect, useState } from "react"; -import { OsQueryState, SearchData, UserRoles } from "shared-types"; -import { createSearchFilterable } from "./utils"; +import { UserRoles, opensearch } from "shared-types"; +import { createSearchFilterable } from "../utils"; import { useQuery } from "@tanstack/react-query"; import { useGetUser } from "@/api/useGetUser"; import { OsTab } from "./types"; @@ -39,12 +39,16 @@ Comments */ export const useOsData = () => { const params = useOsUrl(); - const [data, setData] = useState(); - const { mutateAsync, isLoading, error } = useOsSearch(); - const onRequest = async (query: OsQueryState, options?: any) => { + const [data, setData] = useState(); + const { mutateAsync, isLoading, error } = useOsSearch< + opensearch.main.Field, + opensearch.main.Response + >(); + const onRequest = async (query: opensearch.main.State, options?: any) => { try { await mutateAsync( { + index: "main", pagination: query.pagination, ...(!query.search && { sort: query.sort }), filters: [ @@ -71,7 +75,8 @@ export const useOsAggregate = () => { refetchOnWindowFocus: false, queryKey: [state.tab], queryFn: (props) => { - return getSearchData({ + return getOsData({ + index: "main", aggs: [ { field: "state.keyword", @@ -125,7 +130,7 @@ export const useOsAggregate = () => { }); return aggs.data?.aggregations; }; -export type OsUrlState = OsQueryState & { tab: OsTab }; +export type OsUrlState = opensearch.main.State & { tab: OsTab }; export const useOsUrl = () => { return useLzUrl({ key: "os", diff --git a/src/services/ui/src/components/Opensearch/types.ts b/src/services/ui/src/components/Opensearch/types.ts deleted file mode 100644 index 4541d7170b..0000000000 --- a/src/services/ui/src/components/Opensearch/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { OsField, OsFilterable } from "shared-types"; - -export type OsDrawerFilterable = OsFilterable & { open: boolean }; -export type OsFilterComponentType = "multiSelect" | "multiCheck" | "dateRange"; -export type OsFilterGroup = { - label: string; - field: OsField; - type: OsFilterComponentType; -}; - -export type OsTab = "waivers" | "spas"; - -export { OsField }; diff --git a/src/services/ui/src/components/Opensearch/utils.ts b/src/services/ui/src/components/Opensearch/utils.ts index d2b3f21f7d..29610573f8 100644 --- a/src/services/ui/src/components/Opensearch/utils.ts +++ b/src/services/ui/src/components/Opensearch/utils.ts @@ -1,18 +1,18 @@ -import { OsAggQuery, OsFilterable, OsQueryState } from "shared-types"; -import { OsUrlState } from "./useOpensearch"; +import { opensearch } from "shared-types"; const filterMapQueryReducer = ( - state: Record, - filter: OsFilterable + state: Record["prefix"], any[]>, + filter: opensearch.Filterable ) => { - if (!filter.value) return state; - + // this was hoisted up since false is a valid "match" value if (filter.type === "match") { state[filter.prefix].push({ match: { [filter.field]: filter.value }, }); } + if (!filter.value) return state; + if (filter.type === "terms") { state[filter.prefix].push({ terms: { [filter.field]: filter.value }, @@ -40,7 +40,7 @@ const filterMapQueryReducer = ( return state; }; -export const filterQueryBuilder = (filters: OsFilterable[]) => { +export const filterQueryBuilder = (filters: opensearch.Filterable[]) => { if (!filters?.length) return {}; return { @@ -56,7 +56,7 @@ export const filterQueryBuilder = (filters: OsFilterable[]) => { }; export const paginationQueryBuilder = ( - pagination: OsQueryState["pagination"] + pagination: opensearch.QueryState["pagination"] ) => { const from = (() => { if (!pagination.number) return 0; @@ -69,11 +69,11 @@ export const paginationQueryBuilder = ( }; }; -export const sortQueryBuilder = (sort: OsQueryState["sort"]) => { +export const sortQueryBuilder = (sort: opensearch.QueryState["sort"]) => { return { sort: [{ [sort.field]: sort.order }] }; }; -export const aggQueryBuilder = (aggs: OsAggQuery[]) => { +export const aggQueryBuilder = (aggs: opensearch.AggQuery[]) => { return { aggs: aggs.reduce((STATE, AGG) => { STATE[AGG.name] = { @@ -96,24 +96,14 @@ export const createSearchFilterable = (value?: string) => { field: "", value, prefix: "must", - } as unknown as OsFilterable, + } as unknown as opensearch.Filterable, ]; }; -export const resetFilters = ( - onSet: ( - arg: (arg: OsUrlState) => OsUrlState, - shouldIsolate?: boolean | undefined - ) => void +export const checkMultiFilter = ( + filters: opensearch.Filterable[], + val: number ) => { - onSet((s) => ({ - ...s, - filters: [], - pagination: { ...s.pagination, number: 0 }, - })); -}; - -export const checkMultiFilter = (filters: OsFilterable[], val: number) => { return ( filters.length >= val || filters.some( diff --git a/src/services/ui/src/components/PackageDetails/ReviewTeamList.tsx b/src/services/ui/src/components/PackageDetails/ReviewTeamList.tsx index edb2b3c8d8..e5561328fc 100644 --- a/src/services/ui/src/components/PackageDetails/ReviewTeamList.tsx +++ b/src/services/ui/src/components/PackageDetails/ReviewTeamList.tsx @@ -15,7 +15,7 @@ export const ReviewTeamList = ({ team }: { team: string[] | undefined }) => {

  • {reviewer}
  • ))} {team && team?.length > 3 && ( -
  • +
  • diff --git a/src/services/ui/src/components/Pagination/index.tsx b/src/services/ui/src/components/Pagination/index.tsx index f326bf4dd8..8307b572f5 100644 --- a/src/services/ui/src/components/Pagination/index.tsx +++ b/src/services/ui/src/components/Pagination/index.tsx @@ -63,6 +63,7 @@ export const Pagination: FC = (props) => { viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" + display="none" > = (props) => { {state.pageRange.map((PAGE) => { if (Array.isArray(PAGE)) return ( - +
  • ); const isActive = props.pageNumber === PAGE - 1; @@ -132,6 +133,7 @@ export const Pagination: FC = (props) => { viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" + display="none" > (props: {
    - +

    {props.document.header} - +

    {props.document.sections.map((SEC, index) => ( + {label && {label}} + {descriptionAbove && ( + + {description} + + )} <> {/* ----------------------------------------------------------------------------- */} {rhf === "Input" && (() => { const hops = props as RHFComponentMap["Input"]; - return ; + return ; })()} {/* ----------------------------------------------------------------------------- */} {rhf === "Textarea" && (() => { const hops = props as RHFComponentMap["Textarea"]; - return