diff --git a/frontend-react/src/components/CodeMapping/CodeMappingForm.tsx b/frontend-react/src/components/CodeMapping/CodeMappingForm.tsx index 4abe85e9586..9cbd24aad2a 100644 --- a/frontend-react/src/components/CodeMapping/CodeMappingForm.tsx +++ b/frontend-react/src/components/CodeMapping/CodeMappingForm.tsx @@ -1,33 +1,24 @@ import { Button, ButtonGroup, FileInput } from "@trussworks/react-uswds"; -import { FormEventHandler, MouseEventHandler, type PropsWithChildren, useCallback } from "react"; -import CodeMappingResults from "./CodeMappingResults"; +import { FormEventHandler, MouseEventHandler, useCallback } from "react"; import site from "../../content/site.json"; -import useCodeMappingFormSubmit from "../../hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit"; -import Spinner from "../Spinner"; -import { USLink } from "../USLink"; -export type CodeMappingFormProps = PropsWithChildren; +interface CodeMappingFormProps { + onSubmit: FormEventHandler; + setFileName: (fileName: string) => void; +} -const CodeMappingForm = (props: CodeMappingFormProps) => { - const { data, isPending, mutate } = useCodeMappingFormSubmit(); - /** - * TODO: Implement submit handler - */ - const onSubmitHandler = useCallback>( - (ev) => { - ev.preventDefault(); - mutate(); - return false; - }, - [mutate], - ); +const CodeMappingForm = ({ onSubmit, setFileName }: CodeMappingFormProps) => { const onBackHandler = useCallback((_ev) => { window.history.back(); }, []); - const onCancelHandler = useCallback((_ev) => { - // Don't have a proper mechanism to cancel in-flight requests so refresh page - window.location.reload(); - }, []); + + const handleFileChange = (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + // Take the first file name + setFileName(files[0].name); + }; return ( <> @@ -40,41 +31,22 @@ const CodeMappingForm = (props: CodeMappingFormProps) => { our template to format your result and organism codes to LOINC or SNOMED. Note: Local codes cannot be automatically mapped.

- {data && } - {isPending && ( - <> - -

- Checking your file for any unmapped codes that will
prevent data from being reported - successfully -

- - - )} - {!data && !isPending && ( - - - - Make sure your file has a .csv extension - - - - - - - - )} - {props.children} -

- Questions or feedback? Please email{" "} - reportstream@cdc.gov -

+ + + ); }; diff --git a/frontend-react/src/components/CodeMapping/CodeMappingResults.tsx b/frontend-react/src/components/CodeMapping/CodeMappingResults.tsx index a61595508bb..e6cc755e50e 100644 --- a/frontend-react/src/components/CodeMapping/CodeMappingResults.tsx +++ b/frontend-react/src/components/CodeMapping/CodeMappingResults.tsx @@ -1,10 +1,69 @@ -import type { PropsWithChildren } from "react"; +import { Button } from "@trussworks/react-uswds"; +import site from "../../content/site.json"; +import { CodeMapData } from "../../hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit"; +import { Alert, Table } from "../../shared"; +import { USExtLink } from "../USLink"; -export type CodeMappingResultsProps = PropsWithChildren; +interface CodeMappingResultsProps { + fileName: string; + data: CodeMapData[]; + onReset: () => void; +} -/** - * TODO: Implement result page - */ -const CodeMappingResults = (props: CodeMappingResultsProps) => <>TODO {props.children}; +const CodeMappingResults = ({ fileName, data, onReset }: CodeMappingResultsProps) => { + const unmappedData = data.filter((item: CodeMapData) => item.mapped === "N"); + const areCodesMapped = unmappedData.length === 0; + const rowData = unmappedData.map((dataRow) => [ + { + columnKey: "Code", + columnHeader: "Code", + content: dataRow["test code"], + }, + { + columnKey: "Name", + columnHeader: "Name", + content: dataRow["test description"], + }, + { + columnKey: "Coding system", + columnHeader: "Coding system", + content: dataRow["coding system"], + }, + ]); + return ( + <> +

+ +

File Name

+

{fileName}

+
+

+
+ {areCodesMapped ? ( + + ) : ( + + Review unmapped codes for any user error, such as a typo. If the unmapped codes are accurate, + download the table and send the file to your onboarding engineer or{" "} + {site.orgs.RS.email} . Our team + will support any remaining mapping needed. + + )} +
+

Unmapped codes

+
+ + + + Follow these instructions and use{" "} + our template to format your result and organism codes to + LOINC or SNOMED. + + + + ); +}; export default CodeMappingResults; diff --git a/frontend-react/src/hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit.ts b/frontend-react/src/hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit.ts index 61d78969a82..ad4aeea3f43 100644 --- a/frontend-react/src/hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit.ts +++ b/frontend-react/src/hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit.ts @@ -1,16 +1,43 @@ import { useMutation } from "@tanstack/react-query"; -/** - * TODO: Implement hook - */ +export interface CodeMapData { + "test code": string; + "test description": string; + "coding system": string; + mapped: string; +} + const useCodeMappingFormSubmit = () => { const fn = async () => { - // Fake request until implementation + // Simulate network request await new Promise((resolve) => setTimeout(resolve, 2500)); + + // Return sample JSON + return [ + { + "test code": "97097-0", + "test description": + "SARS-CoV-2 (COVID-19) Ag [Presence] in Upper respiratory specimen by Rapid immunoassay", + "coding system": "LOINC", + mapped: "Y", + }, + { + "test code": "80382-5", + "test description": + "Influenza virus A Ag [Presence] in Upper respiratory specimen by Rapid immunoassay", + "coding system": "LOINC", + mapped: "Y", + }, + { + "test code": "12345", + "test description": "Flu B", + "coding system": "LOCAL", + mapped: "N", + }, + ]; }; - return useMutation({ - mutationFn: fn, - }); + + return useMutation({ mutationFn: fn }); }; export default useCodeMappingFormSubmit; diff --git a/frontend-react/src/pages/onboarding/CodeMappingPage.tsx b/frontend-react/src/pages/onboarding/CodeMappingPage.tsx index 9691a5ccc26..95d52ebe5c7 100644 --- a/frontend-react/src/pages/onboarding/CodeMappingPage.tsx +++ b/frontend-react/src/pages/onboarding/CodeMappingPage.tsx @@ -1,14 +1,73 @@ -import type { PropsWithChildren } from "react"; +import { Button, GridContainer } from "@trussworks/react-uswds"; +import { FormEventHandler, MouseEventHandler, useCallback, useState } from "react"; +import { Helmet } from "react-helmet-async"; import CodeMappingForm from "../../components/CodeMapping/CodeMappingForm"; +import CodeMappingResults from "../../components/CodeMapping/CodeMappingResults"; +import Spinner from "../../components/Spinner"; +import { USExtLink } from "../../components/USLink"; +import site from "../../content/site.json"; +import useCodeMappingFormSubmit from "../../hooks/api/UseCodeMappingFormSubmit/UseCodeMappingFormSubmit"; -export type CodeMappingPageProps = PropsWithChildren; +enum CodeMappingSteps { + StepOne = "CodeMapFileSelect", + StepTwo = "CodeMapResult", +} -const CodeMappingPage = (props: CodeMappingPageProps) => { +const CodeMappingPage = () => { + const { data, isPending, mutate } = useCodeMappingFormSubmit(); + const [currentCodeMapStep, setCurrentCodeMapStep] = useState(CodeMappingSteps.StepOne); + const [fileName, setFileName] = useState(""); + const onCancelHandler = useCallback((_ev) => { + // Don't have a proper mechanism to cancel in-flight requests so refresh page + window.location.reload(); + }, []); + const onReset = () => { + setCurrentCodeMapStep(CodeMappingSteps.StepOne); + }; + const onSubmit = useCallback>( + (ev) => { + ev.preventDefault(); + mutate(); + setCurrentCodeMapStep(CodeMappingSteps.StepTwo); + return false; + }, + [mutate], + ); return ( <> -

Code mapping tool

- - {props.children} + + Code mapping tool - ReportStream + + + +

Code mapping tool

+ {isPending ? ( + <> + +

+ Checking your file for any unmapped codes that will
prevent data from being reported + successfully +

+ + + ) : ( + <> + {currentCodeMapStep === CodeMappingSteps.StepOne && ( + + )} + {currentCodeMapStep === CodeMappingSteps.StepTwo && ( + + )} + + )} + +

+ Questions or feedback? Please email{" "} + {site.orgs.RS.email}{" "} +

+
); }; diff --git a/frontend-react/src/shared/Table/Table.module.scss b/frontend-react/src/shared/Table/Table.module.scss index b1a1c1ba80f..343459c0102 100644 --- a/frontend-react/src/shared/Table/Table.module.scss +++ b/frontend-react/src/shared/Table/Table.module.scss @@ -89,6 +89,14 @@ font-size: 16px; margin: 0; } + + .gray-table { + background-color: color("gray-5"); + + &.usa-table td { + background-color: color("gray-5"); + } + } } &__StickyHeader { diff --git a/frontend-react/src/shared/Table/Table.tsx b/frontend-react/src/shared/Table/Table.tsx index ecab8a76fcd..a75cfe469a9 100644 --- a/frontend-react/src/shared/Table/Table.tsx +++ b/frontend-react/src/shared/Table/Table.tsx @@ -47,6 +47,7 @@ export interface TableProps { sticky?: boolean; striped?: boolean; rowData: RowData[][]; + gray?: boolean; } const TableHeader = ({ dataContent }: { dataContent: RowData["content"] }) => ( @@ -98,9 +99,7 @@ const SortableTableHeader = ({ onClick={handleHeaderClick} >
-

- {columnHeaderData.columnHeader} -

+

{columnHeaderData.columnHeader}

{}
@@ -136,14 +135,9 @@ const CustomSortableTableHeader = ({ "column-header--sticky": sticky, })} > - @@ -151,17 +145,11 @@ const CustomSortableTableHeader = ({ ); }; -function sortTableData( - activeColumn: string, - rowData: RowData[][], - sortOrder: FilterOptions, -) { +function sortTableData(activeColumn: string, rowData: RowData[][], sortOrder: FilterOptions) { return sortOrder !== FilterOptions.NONE && activeColumn ? rowData.sort((a, b): number => { - const contentColA = - a.find((item) => item.columnKey === activeColumn) ?? ""; - const contentColB = - b.find((item) => item.columnKey === activeColumn) ?? ""; + const contentColA = a.find((item) => item.columnKey === activeColumn) ?? ""; + const contentColB = b.find((item) => item.columnKey === activeColumn) ?? ""; if (sortOrder === FilterOptions.ASC) { return contentColA < contentColB ? 1 : -1; } else { @@ -191,12 +179,7 @@ const SortableTable = ({
{columnHeaders.map((columnHeaderData, index) => { if (apiSortable && !columnHeaderData.columnCustomSort) { - return ( - - ); + return ; } else if ( apiSortable && columnHeaderData.columnCustomSort && @@ -207,12 +190,8 @@ const SortableTable = ({ key={index} columnHeaderData={columnHeaderData} sticky={sticky} - onColumnCustomSort={ - columnHeaderData.columnCustomSort - } - columnCustomSortSettings={ - columnHeaderData.columnCustomSortSettings - } + onColumnCustomSort={columnHeaderData.columnCustomSort} + columnCustomSortSettings={columnHeaderData.columnCustomSortSettings} /> ); } @@ -235,8 +214,7 @@ const SortableTable = ({ return ( {row.map((data, dataIndex) => { - const isActive = - data.columnKey === activeColumn; + const isActive = data.columnKey === activeColumn; return (
{ const classes = classnames("usa-table", { "usa-table--borderless": borderless, @@ -275,29 +254,24 @@ const Table = ({ "usa-table--stacked": stackedStyle === "default", "usa-table--stacked-header": stackedStyle === "headers", "usa-table--striped": striped, + "gray-table": gray, }); const columnHeaders = rowData.flat().filter((rowItemFilter, pos, arr) => { - return ( - arr - .map((rowItemMap) => rowItemMap.columnKey) - .indexOf(rowItemFilter.columnKey) === pos - ); + return arr.map((rowItemMap) => rowItemMap.columnKey).indexOf(rowItemFilter.columnKey) === pos; }); return (
{rowData.length ? ( - {sortable ?? apiSortable ? ( + {(sortable ?? apiSortable) ? ( -

- {header.columnHeader} -

+

{header.columnHeader}

); })}