From 176b2d9426c0c8a7c827332d8fb068dd818db987 Mon Sep 17 00:00:00 2001 From: Zedd Shmais Date: Wed, 18 Oct 2023 17:10:56 -0500 Subject: [PATCH 01/78] update sonar dependencies to be compatible with gradle 8 --- backend/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index 2f2b2a247a..7001ad7b97 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -5,7 +5,7 @@ plugins { id 'java' id 'checkstyle' id 'jacoco' - id 'org.sonarqube' version '4.0.0.2929' + id 'org.sonarqube' version '4.4.1.3373' id 'com.gorylenko.gradle-git-properties' version '2.4.1' id "com.diffplug.spotless" version "6.22.0" } @@ -198,7 +198,7 @@ liquibase { } } -sonarqube { +sonar { properties { property "sonar.projectKey", "CDCgov_prime-data-input-client" property "sonar.organization", "cdcgov" From 88e0f45c0e702ff24d50e191765bea1df05609ea Mon Sep 17 00:00:00 2001 From: Zedd Shmais Date: Wed, 18 Oct 2023 17:26:35 -0500 Subject: [PATCH 02/78] update sonar config --- backend/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/build.gradle b/backend/build.gradle index 7001ad7b97..58ffd55153 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -209,6 +209,7 @@ sonar { property "sonar.coverage.exclusions", "frontend/src/setupTests.js,frontend/src/index.tsx" property "sonar.cpd.exclusions", "frontend/src/lang/*.ts" property "sonar.javascript.lcov.reportPaths", "frontend/coverage/lcov.info,ops/services/app_functions/report_stream_batched_publisher/functions/coverage/lcov.info" + property "sonar.gradle.skipCompile", "false" } } From b89cfde9cf8379978b138287d5eb49244c6870fa Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 14 Aug 2023 18:07:08 -0400 Subject: [PATCH 03/78] Use storysource addon --- frontend/.storybook/main.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 358ec8a563..29bc3f1320 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -6,6 +6,15 @@ module.exports = { "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app", + { + name: "@storybook/addon-storysource", + options: { + rule: { + test: [/\.stories\.[j|t]sx?$/], + include: [resolve(__dirname, "../src")], + }, + }, + }, ], webpackFinal: async (config) => { config.resolve.alias["@microsoft/applicationinsights-react-js"] = From 1bfac57aa1bb5eedd3cf16d7d8cb43e56a0d2f39 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 14 Aug 2023 18:08:10 -0400 Subject: [PATCH 04/78] Initial TestQueueCard --- .../TestQueueCard/TestQueueCard.stories.tsx | 35 ++++++++++++ .../TestQueueCard/TestQueueCard.test.tsx | 0 .../testQueue/TestQueueCard/TestQueueCard.tsx | 55 +++++++++++++++++++ .../src/app/testQueue/TestQueueCard/index.tsx | 1 + 4 files changed, 91 insertions(+) create mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx create mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx create mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx create mode 100644 frontend/src/app/testQueue/TestQueueCard/index.tsx diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx new file mode 100644 index 0000000000..052e15c6b1 --- /dev/null +++ b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx @@ -0,0 +1,35 @@ +import { Provider } from "react-redux"; +import { MemoryRouter } from "react-router-dom"; +import { Meta, StoryFn } from "@storybook/react"; + +import { store } from "../../store"; +import { StoryGraphQLProvider } from "../../../stories/storyMocks"; + +import { TestQueueCard } from "./TestQueueCard"; + +type Props = {}; + +export default { + title: "App/Test Queue/Test Queue Card", + component: TestQueueCard, + argTypes: {}, + args: {}, + decorators: [ + (Story) => ( + + + + ), + ], +} as Meta; + +const Template: StoryFn = (args) => ( + + + + + +); + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx new file mode 100644 index 0000000000..7e3eaaa55e --- /dev/null +++ b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Accordion, Button } from "@trussworks/react-uswds"; +import { AccordionItemProps } from "@trussworks/react-uswds/lib/components/Accordion/Accordion"; + +import Card from "../../commonComponents/Card/Card"; + +export const TestQueueCard = () => { + const Header = () => ( +
+
Patient Name
+
DOB: 06/8/2023
+
+
Start Timer
+
X
+
+ ); + + const SubmitResultsButton = () => ( + + ); + + const TestInfoForm = () => ( + <> +
Test date and time
+
+
Test device
+
Specimen type
+
+
COVID-19 Result
+
Is the patient pregnant?
+
+ Is the patient currently experiencing any symptoms? +
+ + + ); + + const TestInfoAccordionItem: AccordionItemProps = { + expanded: false, + headingLevel: "h4", + id: "", + title: "Test information", + content: TestInfoForm(), + }; + + return ( + +
+ {/* Header */} +
+ +
+
+ ); +}; diff --git a/frontend/src/app/testQueue/TestQueueCard/index.tsx b/frontend/src/app/testQueue/TestQueueCard/index.tsx new file mode 100644 index 0000000000..683c51254e --- /dev/null +++ b/frontend/src/app/testQueue/TestQueueCard/index.tsx @@ -0,0 +1 @@ +export { TestQueueCard } from "./TestQueueCard"; From 4c2c3efcd218074dc11c2b1ed49de26c6c58d7c4 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 30 Aug 2023 10:39:36 -0400 Subject: [PATCH 05/78] Refactor test card and form --- frontend/.storybook/main.js | 3 + .../app/commonComponents/YesNoRadioGroup.tsx | 2 +- .../TestCard/CovidResultInputGroup.tsx | 90 +++++ .../TestCard/MultiplexResultInputGroup.tsx | 375 ++++++++++++++++++ .../src/app/testQueue/TestCard/TestCard.scss | 3 + .../testQueue/TestCard/TestCard.stories.tsx | 169 ++++++++ .../src/app/testQueue/TestCard/TestCard.tsx | 148 +++++++ .../TestCard/TestCardFormReducer.tsx | 158 ++++++++ .../src/app/testQueue/TestCard/TestForm.tsx | 319 +++++++++++++++ .../TestQueueCard/TestQueueCard.stories.tsx | 35 -- .../TestQueueCard/TestQueueCard.test.tsx | 0 .../testQueue/TestQueueCard/TestQueueCard.tsx | 55 --- .../src/app/testQueue/TestQueueCard/index.tsx | 1 - frontend/src/app/testQueue/TestTimer.scss | 16 +- frontend/src/app/testQueue/TestTimer.tsx | 23 +- 15 files changed, 1292 insertions(+), 105 deletions(-) create mode 100644 frontend/src/app/testQueue/TestCard/CovidResultInputGroup.tsx create mode 100644 frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx create mode 100644 frontend/src/app/testQueue/TestCard/TestCard.scss create mode 100644 frontend/src/app/testQueue/TestCard/TestCard.stories.tsx create mode 100644 frontend/src/app/testQueue/TestCard/TestCard.tsx create mode 100644 frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx create mode 100644 frontend/src/app/testQueue/TestCard/TestForm.tsx delete mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx delete mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx delete mode 100644 frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx delete mode 100644 frontend/src/app/testQueue/TestQueueCard/index.tsx diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 29bc3f1320..14d1109b44 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -28,4 +28,7 @@ module.exports = { name: "@storybook/react-webpack5", options: { fastRefresh: true }, }, + typescript: { + reactDocgen: "react-docgen", + }, }; diff --git a/frontend/src/app/commonComponents/YesNoRadioGroup.tsx b/frontend/src/app/commonComponents/YesNoRadioGroup.tsx index 523284ea6a..ac79bb3879 100644 --- a/frontend/src/app/commonComponents/YesNoRadioGroup.tsx +++ b/frontend/src/app/commonComponents/YesNoRadioGroup.tsx @@ -57,7 +57,7 @@ const YesNoRadioGroup: React.FC = ({ errorMessage, required, }) => { - const { YES_NO_UNKNOWN_VALUES: values } = useTranslatedConstants(); + const { YES_NO_VALUES: values } = useTranslatedConstants(); return ( { + return ( + (findResultByDiseaseName( + multiplexResultInputs ?? [], + MULTIPLEX_DISEASES.COVID_19 + ) as TestResult) ?? TEST_RESULTS.UNKNOWN + ); +}; + +const convertFromCovidResult = (covidResult: TestResult): CovidResult[] => { + const covidResults: CovidResult[] = [ + { + diseaseName: MULTIPLEX_DISEASES.COVID_19, + testResult: covidResult, + }, + ]; + + return covidResults.filter( + (result) => result.testResult !== TEST_RESULTS.UNKNOWN + ); +}; + +interface Props { + queueItemId: string; + testResults: MultiplexResultInput[]; + isSubmitDisabled?: boolean; + onChange: (value: CovidResult[]) => void; +} + +const CovidResultInputGroup: React.FC = ({ + queueItemId, + testResults, + isSubmitDisabled, + onChange, +}) => { + const resultCovidFormat = convertFromMultiplexResultInputs(testResults); + + const convertAndSendResults = (covidResult: TestResult) => { + const results = convertFromCovidResult(covidResult); + onChange(results); + }; + + return ( +
+

COVID-19 results

+ { + convertAndSendResults(value as TestResult); + }} + buttons={[ + { + value: COVID_RESULTS.POSITIVE, + label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, + }, + { + value: COVID_RESULTS.NEGATIVE, + label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, + }, + { + value: COVID_RESULTS.INCONCLUSIVE, + label: `${TEST_RESULT_DESCRIPTIONS.UNDETERMINED}`, + }, + ]} + name={`covid-test-result-${queueItemId}`} + selectedRadio={resultCovidFormat} + wrapperClassName="prime-radio__group" + disabled={isSubmitDisabled} + /> + + ); +}; + +export default CovidResultInputGroup; diff --git a/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx new file mode 100644 index 0000000000..1ee471be0a --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx @@ -0,0 +1,375 @@ +import React from "react"; + +import RadioGroup from "../../commonComponents/RadioGroup"; +import { COVID_RESULTS, TEST_RESULT_DESCRIPTIONS } from "../../constants"; +import { DevicesMap, findResultByDiseaseName } from "../QueueItem"; +import { TextWithTooltip } from "../../commonComponents/TextWithTooltip"; +import Checkboxes from "../../commonComponents/Checkboxes"; +import { MultiplexResultInput } from "../../../generated/graphql"; +import { MULTIPLEX_DISEASES, TEST_RESULTS } from "../../testResults/constants"; + +const MULTIPLEX_DISEASE_TYPE = { + COVID: MULTIPLEX_DISEASES.COVID_19 as MultiplexDisease, + FLU_A: MULTIPLEX_DISEASES.FLU_A as MultiplexDisease, + FLU_B: MULTIPLEX_DISEASES.FLU_B as MultiplexDisease, + ALL: "All", +}; + +interface MultiplexResult { + diseaseName: MultiplexDisease; + testResult: TestResult; +} + +interface MultiplexResultState { + covid: TestResult; + fluA: TestResult; + fluB: TestResult; +} + +const convertFromMultiplexResultInputs = ( + diseaseResults: MultiplexResultInput[] +): MultiplexResultState => { + const multiplexResult: MultiplexResultState = { + covid: + (findResultByDiseaseName( + diseaseResults ?? [], + MULTIPLEX_DISEASES.COVID_19 + ) as TestResult) ?? TEST_RESULTS.UNKNOWN, + fluA: + (findResultByDiseaseName( + diseaseResults ?? [], + MULTIPLEX_DISEASES.FLU_A + ) as TestResult) ?? TEST_RESULTS.UNKNOWN, + fluB: + (findResultByDiseaseName( + diseaseResults ?? [], + MULTIPLEX_DISEASES.FLU_B + ) as TestResult) ?? TEST_RESULTS.UNKNOWN, + }; + + return multiplexResult; +}; + +const convertFromMultiplexResult = ( + multiplexResult: MultiplexResultState +): MultiplexResult[] => { + const diseaseResults: MultiplexResult[] = [ + { + diseaseName: MULTIPLEX_DISEASE_TYPE.COVID, + testResult: multiplexResult.covid, + }, + { + diseaseName: MULTIPLEX_DISEASE_TYPE.FLU_A, + testResult: multiplexResult.fluA, + }, + { + diseaseName: MULTIPLEX_DISEASE_TYPE.FLU_B, + testResult: multiplexResult.fluB, + }, + ]; + + return diseaseResults.filter( + (result) => result.testResult !== TEST_RESULTS.UNKNOWN + ); +}; + +const isDeviceFluOnly = (deviceId: string, devicesMap: DevicesMap) => { + if (devicesMap.has(deviceId)) { + return devicesMap + .get(deviceId)! + .supportedDiseaseTestPerformed.every( + (disease) => + disease.supportedDisease.name === MULTIPLEX_DISEASES.FLU_A || + disease.supportedDisease.name === MULTIPLEX_DISEASES.FLU_B + ); + } + return false; +}; + +const doesDeviceSupportMultiplexAndCovidOnlyResult = ( + deviceId: string, + devicesMap: DevicesMap +) => { + if (devicesMap.has(deviceId)) { + const deviceTypeCovidDiseases = devicesMap + .get(deviceId)! + .supportedDiseaseTestPerformed.filter( + (disease) => + disease.supportedDisease.name === MULTIPLEX_DISEASES.COVID_19 + ); + + if (deviceTypeCovidDiseases.length >= 1) { + const testPerformedLoincs = [ + ...new Set( + deviceTypeCovidDiseases.map((value) => value.testPerformedLoincCode) + ), + ].filter((item): item is string => !!item); + const testOrderedLoincs = [ + ...new Set( + deviceTypeCovidDiseases.map((value) => value.testOrderedLoincCode) + ), + ].filter((item): item is string => !!item); + const hasSingleCovidTestPerformedLoinc = testPerformedLoincs.length === 1; + const hasMultipleCovidTestOrderedLoincs = testOrderedLoincs.length > 1; + return ( + hasSingleCovidTestPerformedLoinc && hasMultipleCovidTestOrderedLoincs + ); + } + } + return false; +}; + +/** + * COMPONENT + */ +interface Props { + queueItemId: string; + testResults: MultiplexResultInput[]; + deviceId: string; + devicesMap: DevicesMap; + onChange: (value: MultiplexResult[]) => void; +} + +const MultiplexResultInputGroup: React.FC = ({ + queueItemId, + testResults, + deviceId, + devicesMap, + onChange, +}) => { + //eslint-disable-next-line no-restricted-globals + const isMobile = screen.width <= 600; + const resultsMultiplexFormat: MultiplexResultState = + convertFromMultiplexResultInputs(testResults); + let inconclusiveCheck = + resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + + const deviceSupportsCovidOnlyResult = + doesDeviceSupportMultiplexAndCovidOnlyResult(deviceId, devicesMap); + const isFluOnly = isDeviceFluOnly(deviceId, devicesMap); + + if (isFluOnly) { + inconclusiveCheck = + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED; + } + + /** + * Handle Setting Results + */ + const setMultiplexResultInput = ( + diseaseName: "covid" | "fluA" | "fluB", + value: TestResult + ) => { + let newResults: MultiplexResultState = resultsMultiplexFormat; + if (inconclusiveCheck) { + newResults = { + covid: TEST_RESULTS.UNKNOWN, + fluA: TEST_RESULTS.UNKNOWN, + fluB: TEST_RESULTS.UNKNOWN, + }; + } + newResults[diseaseName] = value; + convertAndSendResults(newResults); + }; + + const convertAndSendResults = (multiplexResults: MultiplexResultState) => { + const results = convertFromMultiplexResult(multiplexResults); + onChange(results); + }; + + /** + * Handle Inconclusive + */ + + const handleInconclusiveSelection = (value: any) => { + const markedInconclusive = value.target.checked; + if (markedInconclusive) { + const inconclusiveState: MultiplexResultState = { + covid: isFluOnly ? TEST_RESULTS.UNKNOWN : TEST_RESULTS.UNDETERMINED, + fluA: TEST_RESULTS.UNDETERMINED, + fluB: TEST_RESULTS.UNDETERMINED, + }; + convertAndSendResults(inconclusiveState); + } else { + const currentState = { ...resultsMultiplexFormat }; + if (currentState.covid === TEST_RESULTS.UNDETERMINED) { + currentState.covid = TEST_RESULTS.UNKNOWN; + } + if (currentState.fluA === TEST_RESULTS.UNDETERMINED) { + currentState.fluA = TEST_RESULTS.UNKNOWN; + } + if (currentState.fluB === TEST_RESULTS.UNDETERMINED) { + currentState.fluB = TEST_RESULTS.UNKNOWN; + } + convertAndSendResults(currentState); + } + }; + + /** + * Form Validation + * */ + const validateForm = () => { + let anyResultIsInconclusive = + resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + + let allResultsAreEqual = + resultsMultiplexFormat.covid === resultsMultiplexFormat.fluA && + resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; + + if (isFluOnly) { + allResultsAreEqual = + resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; + anyResultIsInconclusive = + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + } + + const covidIsFilled = + resultsMultiplexFormat.covid === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.covid === TEST_RESULTS.NEGATIVE; + + const fluAIsFilled = + resultsMultiplexFormat.fluA === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.fluA === TEST_RESULTS.NEGATIVE; + + const fluBIsFilled = + resultsMultiplexFormat.fluB === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.fluB === TEST_RESULTS.NEGATIVE; + + if (anyResultIsInconclusive && !allResultsAreEqual) { + return false; + } + return ( + inconclusiveCheck || + (deviceSupportsCovidOnlyResult && + covidIsFilled && + !fluAIsFilled && + !fluBIsFilled) || + (isFluOnly && fluAIsFilled && fluBIsFilled) || + (covidIsFilled && fluAIsFilled && fluBIsFilled) + ); + }; + + return ( +
+
+ {!isFluOnly && ( +
+

COVID-19

+ { + setMultiplexResultInput("covid", value); + }} + buttons={[ + { + value: COVID_RESULTS.POSITIVE, + label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, + }, + { + value: COVID_RESULTS.NEGATIVE, + label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, + }, + ]} + name={`covid-test-result-${queueItemId}`} + selectedRadio={resultsMultiplexFormat.covid} + wrapperClassName="prime-radio__group" + /> +
+ )} +
+

Flu A

+ { + setMultiplexResultInput("fluA", value); + }} + buttons={[ + { + value: COVID_RESULTS.POSITIVE, + label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, + }, + { + value: COVID_RESULTS.NEGATIVE, + label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, + }, + ]} + name={`flu-a-test-result-${queueItemId}`} + selectedRadio={resultsMultiplexFormat.fluA} + wrapperClassName="prime-radio__group" + /> +
+
+

Flu B

+ { + setMultiplexResultInput("fluB", value); + }} + buttons={[ + { + value: COVID_RESULTS.POSITIVE, + label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, + }, + { + value: COVID_RESULTS.NEGATIVE, + label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, + }, + ]} + name={`flu-b-test-result-${queueItemId}`} + selectedRadio={resultsMultiplexFormat.fluB} + wrapperClassName="prime-radio__group" + /> +
+
+
+
+ +
+
+ +
+
+
+ ); +}; + +export default MultiplexResultInputGroup; diff --git a/frontend/src/app/testQueue/TestCard/TestCard.scss b/frontend/src/app/testQueue/TestCard/TestCard.scss new file mode 100644 index 0000000000..77a57730b2 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCard.scss @@ -0,0 +1,3 @@ +.list-style-none { + list-style: none; +} diff --git a/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx new file mode 100644 index 0000000000..572dc3b5a2 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx @@ -0,0 +1,169 @@ +import { Provider } from "react-redux"; +import { MemoryRouter } from "react-router-dom"; +import { Meta, StoryFn } from "@storybook/react"; + +import { store } from "../../store"; +import { StoryGraphQLProvider } from "../../../stories/storyMocks"; +import mockSupportedDiseaseCovid from "../mocks/mockSupportedDiseaseCovid"; +import { PhoneType } from "../../../generated/graphql"; +import mockSupportedDiseaseMultiplex from "../mocks/mockSupportedDiseaseMultiplex"; +import { DevicesMap } from "../QueueItem"; + +import { TestCard, TestCardProps } from "./TestCard"; + +type Props = {}; + +export default { + title: "App/Test Queue/Test Card", + component: TestCard, + argTypes: {}, + args: {}, + decorators: [ + (Story) => ( + + + + ), + ], +} as Meta; + +const Template: StoryFn = (args: TestCardProps) => ( + + + + + +); + +const facilityInfo = { + id: "f02cfff5-1921-4293-beff-e2a5d03e1fda", + name: "Testing Site", + deviceTypes: [ + { + internalId: "ee4f40b7-ac32-4709-be0a-56dd77bb9609", + name: "LumiraDX", + testLength: 15, + supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, + swabTypes: [ + { + name: "Swab of internal nose", + internalId: "8596682d-6053-4720-8a39-1f5d19ff4ed9", + typeCode: "445297001", + }, + { + name: "Nasopharyngeal swab", + internalId: "f127ef55-4133-4556-9bca-33615d071e8d", + typeCode: "258500001", + }, + ], + }, + { + internalId: "5c711888-ba37-4b2e-b347-311ca364efdb", + name: "Abbott BinaxNow", + testLength: 15, + supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, + swabTypes: [ + { + name: "Swab of internal nose", + internalId: "8596682d-6053-4720-8a39-1f5d19ff4ed9", + typeCode: "445297001", + }, + ], + }, + { + internalId: "32b2ca2a-75e6-4ebd-a8af-b50c7aea1d10", + name: "BD Veritor", + testLength: 15, + supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, + swabTypes: [ + { + name: "Swab of internal nose", + internalId: "8596682d-6053-4720-8a39-1f5d19ff4ed9", + typeCode: "445297001", + }, + { + name: "Nasopharyngeal swab", + internalId: "f127ef55-4133-4556-9bca-33615d071e8d", + typeCode: "258500001", + }, + ], + }, + { + internalId: "67109f6f-eaee-49d3-b8ff-c61b79a9da8e", + name: "Multiplex", + testLength: 15, + supportedDiseaseTestPerformed: mockSupportedDiseaseMultiplex, + swabTypes: [ + { + name: "Swab of internal nose", + internalId: "8596682d-6053-4720-8a39-1f5d19ff4ed9", + typeCode: "445297001", + }, + { + name: "Nasopharyngeal swab", + internalId: "f127ef55-4133-4556-9bca-33615d071e8d", + typeCode: "258500001", + }, + ], + }, + ], +}; + +const devicesMap: DevicesMap = new Map(); +facilityInfo.deviceTypes.map((d) => devicesMap.set(d.internalId, d)); + +const testOrderInfo = { + internalId: "1b02363b-ce71-4f30-a2d6-d82b56a91b39", + pregnancy: null, + dateAdded: "2022-11-08 13:33:07.503", + symptoms: + '{"64531003":"false","103001002":"false","84229001":"false","68235000":"false","426000000":"false","49727002":"false","68962001":"false","422587007":"false","267036007":"false","62315008":"false","43724002":"false","36955009":"false","44169009":"false","422400008":"false","230145002":"false","25064002":"false","162397003":"false"}', + symptomOnset: null, + noSymptoms: null, + deviceType: { + internalId: "ee4f40b7-ac32-4709-be0a-56dd77bb9609", + name: "LumiraDX", + model: "LumiraDx SARS-CoV-2 Ag Test*", + testLength: 15, + supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, + }, + specimenType: { + internalId: "8596682d-6053-4720-8a39-1f5d19ff4ed9", + name: "Swab of internal nose", + typeCode: "445297001", + }, + patient: { + internalId: "72b3ce1e-9d5a-4ad2-9ae8-e1099ed1b7e0", + firstName: "Jennifer", + middleName: "K", + lastName: "Finley", + telephone: "571-867-5309", + birthDate: "2002-07-21", + gender: "refused", + testResultDelivery: null, + preferredLanguage: null, + email: "sywaporoce@mailinator.com", + emails: ["sywaporoce@mailinator.com"], + phoneNumbers: [ + { + type: PhoneType.Mobile, + number: "(553) 223-0559", + }, + { + type: PhoneType.Landline, + number: "(669) 789-0799", + }, + ], + }, + results: [], + dateTested: null, + correctionStatus: "ORIGINAL", + reasonForCorrection: null, +}; + +export const Default = Template.bind({}); +Default.args = { + testOrder: testOrderInfo, + facility: facilityInfo, + devicesMap: devicesMap, +}; diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx new file mode 100644 index 0000000000..c06aafb828 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; +import { Card, CardBody, CardHeader, Icon } from "@trussworks/react-uswds"; +import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import moment from "moment"; + +import { DevicesMap, QueriedFacility, QueriedTestOrder } from "../QueueItem"; +import { displayFullName } from "../../utils"; +import Button from "../../commonComponents/Button/Button"; +import { TestTimerWidget, useTestTimer } from "../TestTimer"; +import { RootState } from "../../store"; + +import TestForm from "./TestForm"; + +export interface TestCardProps { + testOrder: QueriedTestOrder; + facility: QueriedFacility; + devicesMap: DevicesMap; +} + +export const TestCard = ({ + testOrder, + facility, + devicesMap, +}: TestCardProps) => { + const navigate = useNavigate(); + const timer = useTestTimer( + testOrder.internalId, + testOrder.deviceType.testLength + ); + const organization = useSelector( + (state: any) => state.organization as Organization + ); + // const [cardBorder] + + const [isOpen, setIsOpen] = useState(false); + + const timerContext = { + organizationName: organization.name, + facilityName: facility!.name, + patientId: testOrder.patient.internalId, + testOrderId: testOrder.internalId, + }; + + const patientFullName = displayFullName( + testOrder.patient.firstName, + testOrder.patient.middleName, + testOrder.patient.lastName + ); + + const patientDateOfBirth = moment(testOrder.patient.birthDate); + + return ( + + +
+
+
+ +
+
+ +
+
+ + DOB: {patientDateOfBirth.format("MM/DD/YYYY")} + +
+
+
+ +
+
+ +
+
+
+
+ {isOpen && ( + +
+ {/*
*/} + + {/*
Test information
*/} + {/*
*/} + {/**/} + {/* ),*/} + {/* },*/} + {/* ]}*/} + {/*>*/} +
+ +
+
+
+ )} +
+ ); +}; diff --git a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx new file mode 100644 index 0000000000..8eebc957a8 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx @@ -0,0 +1,158 @@ +import moment from "moment/moment"; + +import { + PregnancyCode, + SymptomCode, +} from "../../../patientApp/timeOfTest/constants"; +import { MultiplexResultInput } from "../../../generated/graphql"; +import { DevicesMap } from "../QueueItem"; + +export interface TestFormState { + dateTested?: string; + dirty: boolean; + deviceId: string; + specimenId: string; + testResults: MultiplexResultInput[]; + questions: TestQuestionResponses; + errors: { + dateTested: string; + deviceId: string; + specimenId: string; + }; +} + +export interface TestQuestionResponses { + pregnancy?: PregnancyCode; + // SymptomInputs should probably be updated to use SymptomCode and SymptomName types + symptoms: Record; + symptomOnsetDate?: string; +} + +export enum TestCardFormAction { + UPDATE_DATE_TESTED = "UPDATE_DATE_TESTED", + UPDATE_TIME_TESTED = "UPDATE_TIME_TESTED", + UPDATE_DEVICE_ID = "UPDATE_DEVICE_ID", + UPDATE_SPECIMEN_ID = "UPDATE_SPECIMEN_ID", + UPDATE_TEST_RESULT = "UPDATE_TEST_RESULT", + UPDATE_PREGNANCY = "UPDATE_PREGNANCY", + TOGGLE_SYMPTOM = "TOGGLE_SYMPTOM", + UPDATE_SYMPTOMS = "UPDATE_SYMPTOMS", + UPDATE_SYMPTOM_ONSET_DATE = "UPDATE_SYMPTOM_ONSET_DATE", +} + +export type TestQueueFormAction = + | { type: TestCardFormAction.UPDATE_DATE_TESTED; payload: string } + | { type: TestCardFormAction.UPDATE_TIME_TESTED; payload: string } + | { + type: TestCardFormAction.UPDATE_DEVICE_ID; + payload: { deviceId: string; devicesMap: DevicesMap }; + } + | { type: TestCardFormAction.UPDATE_SPECIMEN_ID; payload: string } + | { + type: TestCardFormAction.UPDATE_TEST_RESULT; + payload: MultiplexResultInput[]; + } + | { type: TestCardFormAction.UPDATE_PREGNANCY; payload: PregnancyCode } + | { type: TestCardFormAction.TOGGLE_SYMPTOM; payload: SymptomCode } + | { + type: TestCardFormAction.UPDATE_SYMPTOMS; + payload: Record; + } + | { type: TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE; payload: string }; + +export const testCardFormReducer = ( + prevState: TestFormState, + { type, payload }: TestQueueFormAction +): TestFormState => { + switch (type) { + case TestCardFormAction.UPDATE_DATE_TESTED: { + // the date string returned from the server is only precise to seconds; moment's + // toISOString method returns millisecond precision. as a result, an onChange event + // was being fired when this component initialized, sending an EditQueueItem to + // the back end w/ the same data that it already had. this prevents it: + if (!moment(prevState.dateTested).isSame(payload)) { + const newDate = moment(payload); + if (prevState.dateTested) { + const prevDateTested = moment(prevState.dateTested); + newDate.hour(prevDateTested.hours()); + newDate.minute(prevDateTested.minutes()); + } + return { + ...prevState, + dateTested: newDate.toISOString(), + dirty: true, + }; + } + break; + } + case TestCardFormAction.UPDATE_TIME_TESTED: { + if (payload) { + const [hours, minutes] = payload.split(":"); + const newDate = moment(prevState.dateTested) + .hours(parseInt(hours)) + .minutes(parseInt(minutes)); + return { + ...prevState, + dateTested: newDate.toISOString(), + dirty: true, + }; + } + break; + } + case TestCardFormAction.UPDATE_DEVICE_ID: { + return { + ...prevState, + deviceId: payload.deviceId, + specimenId: + payload.devicesMap.get(payload.deviceId)?.swabTypes[0].internalId ?? + prevState.specimenId, + dirty: true, + }; + } + case TestCardFormAction.UPDATE_SPECIMEN_ID: { + return { + ...prevState, + specimenId: payload, + dirty: true, + }; + } + case TestCardFormAction.UPDATE_TEST_RESULT: { + console.log(prevState, payload); + return { + ...prevState, + testResults: payload, + dirty: true, + }; + } + case TestCardFormAction.UPDATE_PREGNANCY: { + return { + ...prevState, + questions: { + ...prevState.questions, + pregnancy: payload, + }, + }; + } + case TestCardFormAction.TOGGLE_SYMPTOM: { + return { + ...prevState, + questions: { + ...prevState.questions, + symptoms: { + ...prevState.questions.symptoms, + }, + }, + }; + } + case TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE: { + return { + ...prevState, + questions: { + ...prevState.questions, + symptomOnsetDate: moment(payload).toISOString(), + }, + }; + } + } + throw Error("Unknown action: " + type); +}; diff --git a/frontend/src/app/testQueue/TestCard/TestForm.tsx b/frontend/src/app/testQueue/TestCard/TestForm.tsx new file mode 100644 index 0000000000..ede3612799 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestForm.tsx @@ -0,0 +1,319 @@ +import moment from "moment"; +import { Alert, Button } from "@trussworks/react-uswds"; +import React, { useMemo, useReducer, useState } from "react"; + +import TextInput from "../../commonComponents/TextInput"; +import { DevicesMap, QueriedFacility, QueriedTestOrder } from "../QueueItem"; +import { formatDate } from "../../utils/date"; +import { TextWithTooltip } from "../../commonComponents/TextWithTooltip"; +import Dropdown from "../../commonComponents/Dropdown"; +import { GetFacilityQueueQuery } from "../../../generated/graphql"; +import RadioGroup from "../../commonComponents/RadioGroup"; +import { + getPregnancyResponses, + globalSymptomDefinitions, + SymptomCode, +} from "../../../patientApp/timeOfTest/constants"; +import YesNoRadioGroup from "../../commonComponents/YesNoRadioGroup"; +import Checkboxes from "../../commonComponents/Checkboxes"; +import { MULTIPLEX_DISEASES } from "../../testResults/constants"; + +import { + TestCardFormAction, + testCardFormReducer, + TestFormState, +} from "./TestCardFormReducer"; +import CovidResultInputGroup from "./CovidResultInputGroup"; +import MultiplexResultInputGroup from "./MultiplexResultInputGroup"; + +export interface TestFormProps { + testOrder: QueriedTestOrder; + devicesMap: DevicesMap; + facility: QueriedFacility; +} + +export type QueriedSupportedDiseaseTestPerformed = NonNullable< + NonNullable["deviceTypes"][number] +>["supportedDiseaseTestPerformed"][number]; + +const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { + const initialFormState: TestFormState = { + dirty: false, + dateTested: testOrder.dateTested, + deviceId: testOrder.deviceType.internalId ?? "", + specimenId: testOrder.specimenType.internalId ?? "", + testResults: testOrder.results, + questions: { symptoms: {} }, + errors: { dateTested: "", deviceId: "", specimenId: "" }, + }; + const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); + const [hasAnySymptoms, setHasAnySymptoms] = useState(); + + function alphabetizeByName( + a: DeviceType | SpecimenType, + b: DeviceType | SpecimenType + ): number { + if (a.name < b.name) { + return -1; + } + + if (a.name > b.name) { + return 1; + } + + return 0; + } + + let deviceTypeOptions = useMemo( + () => + [...facility!.deviceTypes].sort(alphabetizeByName).map((d) => ({ + label: d.name, + value: d.internalId, + })), + [facility] + ); + const deviceTypeIsInvalid = !devicesMap.has(state.deviceId); + + if (state.deviceId && !devicesMap.has(state.deviceId)) { + // this adds an empty option for when the device has been deleted from the facility, but it's on the test order + deviceTypeOptions = [{ label: "", value: "" }, ...deviceTypeOptions]; + } + + let specimenTypeOptions = useMemo( + () => + state.deviceId && devicesMap.has(state.deviceId) + ? [...devicesMap.get(state.deviceId)!.swabTypes] + .sort(alphabetizeByName) + .map((s: SpecimenType) => ({ + label: s.name, + value: s.internalId, + })) + : [], + [state.deviceId, devicesMap] + ); + const specimenTypeIsInvalid = + devicesMap.has(state.deviceId) && + devicesMap + .get(state.deviceId)! + .swabTypes.filter((s) => s.internalId === state.specimenId).length === 0; + + if (specimenTypeIsInvalid) { + // this adds an empty option for when the specimen has been deleted from the device, but it's on the test order + specimenTypeOptions = [{ label: "", value: "" }, ...specimenTypeOptions]; + } + + const pregnancyResponses = useMemo(() => getPregnancyResponses(), []); + + const deviceSupportsMultiplex = useMemo(() => { + if (devicesMap.has(state.deviceId)) { + return ( + devicesMap + .get(state.deviceId)! + .supportedDiseaseTestPerformed.filter( + (disease) => + disease.supportedDisease.name !== MULTIPLEX_DISEASES.COVID_19 + ).length > 0 + ); + } + return false; + }, [devicesMap, state.deviceId]); + + const isBeforeDateWarningThreshold = + moment(state.dateTested) < moment().subtract(6, "months"); + + return ( + <> +
+ + "Alert warning" + +
+
+
+ + dispatch({ + type: TestCardFormAction.UPDATE_DATE_TESTED, + payload: e.target.value, + }) + } + disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} + > +
+
+ + dispatch({ + type: TestCardFormAction.UPDATE_TIME_TESTED, + payload: e.target.value, + }) + } + disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} + > +
+
+
+
+ + + + } + name="testDevice" + selectedValue={state.deviceId} + onChange={(e) => + dispatch({ + type: TestCardFormAction.UPDATE_DEVICE_ID, + payload: { deviceId: e.target.value, devicesMap }, + }) + } + className="card-dropdown" + data-testid="device-type-dropdown" + errorMessage={state.errors.deviceId} + validationStatus={state.errors.deviceId ? "error" : "success"} + /> +
+
+ + dispatch({ + type: TestCardFormAction.UPDATE_SPECIMEN_ID, + payload: e.target.value, + }) + } + className="card-dropdown" + data-testid="specimen-type-dropdown" + disabled={specimenTypeOptions.length === 0} + errorMessage={state.errors.specimenId} + validationStatus={state.errors.specimenId ? "error" : "success"} + /> +
+
+
+ {deviceSupportsMultiplex ? ( + + dispatch({ + type: TestCardFormAction.UPDATE_TEST_RESULT, + payload: results, + }) + } + > + ) : ( + + dispatch({ + type: TestCardFormAction.UPDATE_TEST_RESULT, + payload: results, + }) + } + /> + )} +
+
+
+ + dispatch({ + type: TestCardFormAction.UPDATE_PREGNANCY, + payload: pregnancyCode, + }) + } + buttons={pregnancyResponses} + selectedRadio={state.questions.pregnancy} + /> +
+
+
+
+ setHasAnySymptoms(e)} + /> +
+
+ {hasAnySymptoms === "YES" && ( + <> +
+ + dispatch({ + type: TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE, + payload: e.target.value, + }) + } + > +
+
+ + dispatch({ + type: TestCardFormAction.TOGGLE_SYMPTOM, + payload: e.target.value as SymptomCode, + }) + } + /> +
+ + )} +
+
+ +
+
+ + ); +}; + +export default TestForm; diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx deleted file mode 100644 index 052e15c6b1..0000000000 --- a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Provider } from "react-redux"; -import { MemoryRouter } from "react-router-dom"; -import { Meta, StoryFn } from "@storybook/react"; - -import { store } from "../../store"; -import { StoryGraphQLProvider } from "../../../stories/storyMocks"; - -import { TestQueueCard } from "./TestQueueCard"; - -type Props = {}; - -export default { - title: "App/Test Queue/Test Queue Card", - component: TestQueueCard, - argTypes: {}, - args: {}, - decorators: [ - (Story) => ( - - - - ), - ], -} as Meta; - -const Template: StoryFn = (args) => ( - - - - - -); - -export const Default = Template.bind({}); -Default.args = {}; diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.test.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx b/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx deleted file mode 100644 index 7e3eaaa55e..0000000000 --- a/frontend/src/app/testQueue/TestQueueCard/TestQueueCard.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { Accordion, Button } from "@trussworks/react-uswds"; -import { AccordionItemProps } from "@trussworks/react-uswds/lib/components/Accordion/Accordion"; - -import Card from "../../commonComponents/Card/Card"; - -export const TestQueueCard = () => { - const Header = () => ( -
-
Patient Name
-
DOB: 06/8/2023
-
-
Start Timer
-
X
-
- ); - - const SubmitResultsButton = () => ( - - ); - - const TestInfoForm = () => ( - <> -
Test date and time
-
-
Test device
-
Specimen type
-
-
COVID-19 Result
-
Is the patient pregnant?
-
- Is the patient currently experiencing any symptoms? -
- - - ); - - const TestInfoAccordionItem: AccordionItemProps = { - expanded: false, - headingLevel: "h4", - id: "", - title: "Test information", - content: TestInfoForm(), - }; - - return ( - -
- {/* Header */} -
- -
-
- ); -}; diff --git a/frontend/src/app/testQueue/TestQueueCard/index.tsx b/frontend/src/app/testQueue/TestQueueCard/index.tsx deleted file mode 100644 index 683c51254e..0000000000 --- a/frontend/src/app/testQueue/TestQueueCard/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { TestQueueCard } from "./TestQueueCard"; diff --git a/frontend/src/app/testQueue/TestTimer.scss b/frontend/src/app/testQueue/TestTimer.scss index 8bbdc2b3f2..3e699380d2 100644 --- a/frontend/src/app/testQueue/TestTimer.scss +++ b/frontend/src/app/testQueue/TestTimer.scss @@ -1,5 +1,6 @@ +@use "../../scss/settings" as settings; + .timer-button { - border: none; border-radius: 3px; min-width: 80px; cursor: pointer; @@ -8,21 +9,22 @@ display: flex; align-items: center; justify-content: space-around; + border: 1px solid settings.$theme-color-prime-blue; } .timer-reset { - color: white; - background-color: #005ea2; + color: #005ea2; + background-color: white; } .timer-running { - color: white; - background-color: #162e51; + color: #162e51; + background-color: white; } .timer-ready { - color: green; - background-color: white; + color: white; + background-color: green; } .timer-overtime { diff --git a/frontend/src/app/testQueue/TestTimer.tsx b/frontend/src/app/testQueue/TestTimer.tsx index 6c83401607..cd4288f341 100644 --- a/frontend/src/app/testQueue/TestTimer.tsx +++ b/frontend/src/app/testQueue/TestTimer.tsx @@ -4,6 +4,8 @@ import { faStopwatch, faRedo } from "@fortawesome/free-solid-svg-icons"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import "./TestTimer.scss"; +import { Button } from "@trussworks/react-uswds"; + import { getAppInsights } from "../../app/TelemetryService"; const alarmModule = require("./test-timer.mp3"); @@ -15,12 +17,14 @@ type DateTimeStamp = ReturnType; function toMillis(minutes: number) { return minutes * 60 * 1000; } + export interface TimerTrackEventMetadata { facilityName: string | undefined; organizationName: string; patientId: string; testOrderId: string; } + export class Timer { id: string; startedAt: DateTimeStamp; @@ -234,15 +238,22 @@ export const TestTimerWidget = ({ timer, context }: Props) => { if (!running) { return ( - + + + Start timer + {" "} + ); } if (countdown >= 0) { From df07aee0c3b7f8edd59b1cee17efb098c426e2d6 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 30 Aug 2023 10:48:58 -0400 Subject: [PATCH 06/78] Use TestCard.scss --- frontend/src/app/testQueue/TestCard/TestCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index c06aafb828..13cb094111 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -11,6 +11,7 @@ import { TestTimerWidget, useTestTimer } from "../TestTimer"; import { RootState } from "../../store"; import TestForm from "./TestForm"; +import "./TestCard.scss"; export interface TestCardProps { testOrder: QueriedTestOrder; From 9922228a948804776a710735e974239879d14e41 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 5 Sep 2023 18:40:30 -0400 Subject: [PATCH 07/78] Change accordion to collapsible card --- .../src/app/testQueue/TestCard/TestCard.tsx | 57 ++++--------- .../{TestForm.tsx => TestCardForm.tsx} | 79 ++++++++++--------- .../TestCard/TestCardFormReducer.tsx | 40 +++++----- 3 files changed, 77 insertions(+), 99 deletions(-) rename frontend/src/app/testQueue/TestCard/{TestForm.tsx => TestCardForm.tsx} (87%) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 13cb094111..c46e44b20e 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -10,7 +10,7 @@ import Button from "../../commonComponents/Button/Button"; import { TestTimerWidget, useTestTimer } from "../TestTimer"; import { RootState } from "../../store"; -import TestForm from "./TestForm"; +import TestCardForm from "./TestCardForm"; import "./TestCard.scss"; export interface TestCardProps { @@ -32,9 +32,9 @@ export const TestCard = ({ const organization = useSelector( (state: any) => state.organization as Organization ); - // const [cardBorder] const [isOpen, setIsOpen] = useState(false); + const [isCollapsible, setIsCollapsible] = useState(true); const timerContext = { organizationName: organization.name, @@ -51,15 +51,18 @@ export const TestCard = ({ const patientDateOfBirth = moment(testOrder.patient.birthDate); + const toggleOpen = () => setIsOpen((prevState) => !prevState); + return ( - +
-
+
- {isOpen && ( - -
- {/*
*/} - - {/*
Test information
*/} - {/*
*/} - {/**/} - {/* ),*/} - {/* },*/} - {/* ]}*/} - {/*>*/} -
- -
-
-
- )} + +
+ +
+
); }; diff --git a/frontend/src/app/testQueue/TestCard/TestForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx similarity index 87% rename from frontend/src/app/testQueue/TestCard/TestForm.tsx rename to frontend/src/app/testQueue/TestCard/TestCardForm.tsx index ede3612799..8f2b29a6c6 100644 --- a/frontend/src/app/testQueue/TestCard/TestForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -7,7 +7,6 @@ import { DevicesMap, QueriedFacility, QueriedTestOrder } from "../QueueItem"; import { formatDate } from "../../utils/date"; import { TextWithTooltip } from "../../commonComponents/TextWithTooltip"; import Dropdown from "../../commonComponents/Dropdown"; -import { GetFacilityQueueQuery } from "../../../generated/graphql"; import RadioGroup from "../../commonComponents/RadioGroup"; import { getPregnancyResponses, @@ -19,9 +18,9 @@ import Checkboxes from "../../commonComponents/Checkboxes"; import { MULTIPLEX_DISEASES } from "../../testResults/constants"; import { - TestCardFormAction, - testCardFormReducer, + TestFormActionCase, TestFormState, + testCardFormReducer, } from "./TestCardFormReducer"; import CovidResultInputGroup from "./CovidResultInputGroup"; import MultiplexResultInputGroup from "./MultiplexResultInputGroup"; @@ -32,11 +31,24 @@ export interface TestFormProps { facility: QueriedFacility; } -export type QueriedSupportedDiseaseTestPerformed = NonNullable< - NonNullable["deviceTypes"][number] ->["supportedDiseaseTestPerformed"][number]; +function alphabetizeByName( + a: DeviceType | SpecimenType, + b: DeviceType | SpecimenType +): number { + if (a.name < b.name) { + return -1; + } + + if (a.name > b.name) { + return 1; + } + + return 0; +} + +const pregnancyResponses = getPregnancyResponses(); -const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { +const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { const initialFormState: TestFormState = { dirty: false, dateTested: testOrder.dateTested, @@ -49,21 +61,6 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); const [hasAnySymptoms, setHasAnySymptoms] = useState(); - function alphabetizeByName( - a: DeviceType | SpecimenType, - b: DeviceType | SpecimenType - ): number { - if (a.name < b.name) { - return -1; - } - - if (a.name > b.name) { - return 1; - } - - return 0; - } - let deviceTypeOptions = useMemo( () => [...facility!.deviceTypes].sort(alphabetizeByName).map((d) => ({ @@ -102,8 +99,6 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { specimenTypeOptions = [{ label: "", value: "" }, ...specimenTypeOptions]; } - const pregnancyResponses = useMemo(() => getPregnancyResponses(), []); - const deviceSupportsMultiplex = useMemo(() => { if (devicesMap.has(state.deviceId)) { return ( @@ -123,11 +118,17 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { return ( <> -
- - "Alert warning" - -
+ {isBeforeDateWarningThreshold && ( +
+
+ + Check test date: The date you selected is more + than six months ago. Please make sure it's correct before + submitting. + +
+
+ )}
{ value={formatDate(moment(state.dateTested).toDate())} onChange={(e) => dispatch({ - type: TestCardFormAction.UPDATE_DATE_TESTED, + type: TestFormActionCase.UPDATE_DATE_TESTED, payload: e.target.value, }) } @@ -161,7 +162,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { value={moment(state.dateTested).format("HH:mm")} onChange={(e) => dispatch({ - type: TestCardFormAction.UPDATE_TIME_TESTED, + type: TestFormActionCase.UPDATE_TIME_TESTED, payload: e.target.value, }) } @@ -186,7 +187,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { selectedValue={state.deviceId} onChange={(e) => dispatch({ - type: TestCardFormAction.UPDATE_DEVICE_ID, + type: TestFormActionCase.UPDATE_DEVICE_ID, payload: { deviceId: e.target.value, devicesMap }, }) } @@ -204,7 +205,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { selectedValue={state.specimenId} onChange={(e) => dispatch({ - type: TestCardFormAction.UPDATE_SPECIMEN_ID, + type: TestFormActionCase.UPDATE_SPECIMEN_ID, payload: e.target.value, }) } @@ -225,7 +226,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { devicesMap={devicesMap} onChange={(results) => dispatch({ - type: TestCardFormAction.UPDATE_TEST_RESULT, + type: TestFormActionCase.UPDATE_TEST_RESULT, payload: results, }) } @@ -236,7 +237,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { testResults={state.testResults} onChange={(results) => dispatch({ - type: TestCardFormAction.UPDATE_TEST_RESULT, + type: TestFormActionCase.UPDATE_TEST_RESULT, payload: results, }) } @@ -250,7 +251,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { name="pregnancy" onChange={(pregnancyCode) => dispatch({ - type: TestCardFormAction.UPDATE_PREGNANCY, + type: TestFormActionCase.UPDATE_PREGNANCY, payload: pregnancyCode, }) } @@ -286,7 +287,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { )} onChange={(e) => dispatch({ - type: TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE, + type: TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE, payload: e.target.value, }) } @@ -299,7 +300,7 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { name={`symptoms-${testOrder.internalId}`} onChange={(e) => dispatch({ - type: TestCardFormAction.TOGGLE_SYMPTOM, + type: TestFormActionCase.TOGGLE_SYMPTOM, payload: e.target.value as SymptomCode, }) } @@ -316,4 +317,4 @@ const TestForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { ); }; -export default TestForm; +export default TestCardForm; diff --git a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx index 8eebc957a8..d65b869b11 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx @@ -28,7 +28,7 @@ export interface TestQuestionResponses { symptomOnsetDate?: string; } -export enum TestCardFormAction { +export enum TestFormActionCase { UPDATE_DATE_TESTED = "UPDATE_DATE_TESTED", UPDATE_TIME_TESTED = "UPDATE_TIME_TESTED", UPDATE_DEVICE_ID = "UPDATE_DEVICE_ID", @@ -40,32 +40,32 @@ export enum TestCardFormAction { UPDATE_SYMPTOM_ONSET_DATE = "UPDATE_SYMPTOM_ONSET_DATE", } -export type TestQueueFormAction = - | { type: TestCardFormAction.UPDATE_DATE_TESTED; payload: string } - | { type: TestCardFormAction.UPDATE_TIME_TESTED; payload: string } +export type TestFormAction = + | { type: TestFormActionCase.UPDATE_DATE_TESTED; payload: string } + | { type: TestFormActionCase.UPDATE_TIME_TESTED; payload: string } | { - type: TestCardFormAction.UPDATE_DEVICE_ID; + type: TestFormActionCase.UPDATE_DEVICE_ID; payload: { deviceId: string; devicesMap: DevicesMap }; } - | { type: TestCardFormAction.UPDATE_SPECIMEN_ID; payload: string } + | { type: TestFormActionCase.UPDATE_SPECIMEN_ID; payload: string } | { - type: TestCardFormAction.UPDATE_TEST_RESULT; + type: TestFormActionCase.UPDATE_TEST_RESULT; payload: MultiplexResultInput[]; } - | { type: TestCardFormAction.UPDATE_PREGNANCY; payload: PregnancyCode } - | { type: TestCardFormAction.TOGGLE_SYMPTOM; payload: SymptomCode } + | { type: TestFormActionCase.UPDATE_PREGNANCY; payload: PregnancyCode } + | { type: TestFormActionCase.TOGGLE_SYMPTOM; payload: SymptomCode } | { - type: TestCardFormAction.UPDATE_SYMPTOMS; + type: TestFormActionCase.UPDATE_SYMPTOMS; payload: Record; } - | { type: TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE; payload: string }; + | { type: TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE; payload: string }; export const testCardFormReducer = ( prevState: TestFormState, - { type, payload }: TestQueueFormAction + { type, payload }: TestFormAction ): TestFormState => { switch (type) { - case TestCardFormAction.UPDATE_DATE_TESTED: { + case TestFormActionCase.UPDATE_DATE_TESTED: { // the date string returned from the server is only precise to seconds; moment's // toISOString method returns millisecond precision. as a result, an onChange event // was being fired when this component initialized, sending an EditQueueItem to @@ -85,7 +85,7 @@ export const testCardFormReducer = ( } break; } - case TestCardFormAction.UPDATE_TIME_TESTED: { + case TestFormActionCase.UPDATE_TIME_TESTED: { if (payload) { const [hours, minutes] = payload.split(":"); const newDate = moment(prevState.dateTested) @@ -99,7 +99,7 @@ export const testCardFormReducer = ( } break; } - case TestCardFormAction.UPDATE_DEVICE_ID: { + case TestFormActionCase.UPDATE_DEVICE_ID: { return { ...prevState, deviceId: payload.deviceId, @@ -109,14 +109,14 @@ export const testCardFormReducer = ( dirty: true, }; } - case TestCardFormAction.UPDATE_SPECIMEN_ID: { + case TestFormActionCase.UPDATE_SPECIMEN_ID: { return { ...prevState, specimenId: payload, dirty: true, }; } - case TestCardFormAction.UPDATE_TEST_RESULT: { + case TestFormActionCase.UPDATE_TEST_RESULT: { console.log(prevState, payload); return { ...prevState, @@ -124,7 +124,7 @@ export const testCardFormReducer = ( dirty: true, }; } - case TestCardFormAction.UPDATE_PREGNANCY: { + case TestFormActionCase.UPDATE_PREGNANCY: { return { ...prevState, questions: { @@ -133,7 +133,7 @@ export const testCardFormReducer = ( }, }; } - case TestCardFormAction.TOGGLE_SYMPTOM: { + case TestFormActionCase.TOGGLE_SYMPTOM: { return { ...prevState, questions: { @@ -144,7 +144,7 @@ export const testCardFormReducer = ( }, }; } - case TestCardFormAction.UPDATE_SYMPTOM_ONSET_DATE: { + case TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE: { return { ...prevState, questions: { From 6c56822cf4969d7936671463f255a93811e47023 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 5 Sep 2023 19:52:50 -0400 Subject: [PATCH 08/78] Add Covid AoE form --- .../testQueue/TestCard/AoE/CovidAoEForm.tsx | 106 ++++++++++++++++++ .../app/testQueue/TestCard/TestCardForm.tsx | 92 +++------------ .../TestCard/TestCardFormReducer.tsx | 52 ++------- 3 files changed, 132 insertions(+), 118 deletions(-) create mode 100644 frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx diff --git a/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx new file mode 100644 index 0000000000..77fa4a06fa --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx @@ -0,0 +1,106 @@ +import moment from "moment/moment"; +import React, { useState } from "react"; + +import RadioGroup from "../../../commonComponents/RadioGroup"; +import YesNoRadioGroup from "../../../commonComponents/YesNoRadioGroup"; +import TextInput from "../../../commonComponents/TextInput"; +import { formatDate } from "../../../utils/date"; +import Checkboxes from "../../../commonComponents/Checkboxes"; +import { + getPregnancyResponses, + globalSymptomDefinitions, + PregnancyCode, + SymptomCode, +} from "../../../../patientApp/timeOfTest/constants"; +import { QueriedTestOrder } from "../../QueueItem"; +import { CovidAoeQuestionResponses } from "../TestCardFormReducer"; + +export interface CovidAoEFormProps { + testOrder: QueriedTestOrder; + responses: CovidAoeQuestionResponses; + onResponseChange: (responses: CovidAoeQuestionResponses) => void; +} + +const pregnancyResponses = getPregnancyResponses(); + +const CovidAoEForm = ({ + testOrder, + responses, + onResponseChange, +}: CovidAoEFormProps) => { + const [hasAnySymptoms, setHasAnySymptoms] = useState(); + + const onPregnancyChange = (pregnancyCode: PregnancyCode) => { + onResponseChange({ ...responses, pregnancy: pregnancyCode }); + }; + + const onSymptomOnsetDateChange = (symptomOnsetDate: string) => { + onResponseChange({ + ...responses, + symptomOnsetDate: moment(symptomOnsetDate).toISOString(), + }); + }; + + const onSymptomsChange = (symptom: string) => { + let updateSymptoms = { ...responses.symptoms }; + updateSymptoms[symptom] = !updateSymptoms[symptom]; + onResponseChange({ + ...responses, + symptoms: updateSymptoms, + }); + }; + + return ( + <> +
+
+ +
+
+
+
+ setHasAnySymptoms(e)} + /> +
+
+ {hasAnySymptoms === "YES" && ( + <> +
+ onSymptomOnsetDateChange(e.target.value)} + > +
+
+ onSymptomsChange(e.target.value as SymptomCode)} + /> +
+ + )} + + ); +}; + +export default CovidAoEForm; diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index 8f2b29a6c6..bdf03bbbfc 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -1,20 +1,12 @@ import moment from "moment"; import { Alert, Button } from "@trussworks/react-uswds"; -import React, { useMemo, useReducer, useState } from "react"; +import React, { useMemo, useReducer } from "react"; import TextInput from "../../commonComponents/TextInput"; import { DevicesMap, QueriedFacility, QueriedTestOrder } from "../QueueItem"; import { formatDate } from "../../utils/date"; import { TextWithTooltip } from "../../commonComponents/TextWithTooltip"; import Dropdown from "../../commonComponents/Dropdown"; -import RadioGroup from "../../commonComponents/RadioGroup"; -import { - getPregnancyResponses, - globalSymptomDefinitions, - SymptomCode, -} from "../../../patientApp/timeOfTest/constants"; -import YesNoRadioGroup from "../../commonComponents/YesNoRadioGroup"; -import Checkboxes from "../../commonComponents/Checkboxes"; import { MULTIPLEX_DISEASES } from "../../testResults/constants"; import { @@ -24,6 +16,7 @@ import { } from "./TestCardFormReducer"; import CovidResultInputGroup from "./CovidResultInputGroup"; import MultiplexResultInputGroup from "./MultiplexResultInputGroup"; +import CovidAoEForm from "./AoE/CovidAoEForm"; export interface TestFormProps { testOrder: QueriedTestOrder; @@ -46,8 +39,6 @@ function alphabetizeByName( return 0; } -const pregnancyResponses = getPregnancyResponses(); - const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { const initialFormState: TestFormState = { dirty: false, @@ -55,11 +46,10 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { deviceId: testOrder.deviceType.internalId ?? "", specimenId: testOrder.specimenType.internalId ?? "", testResults: testOrder.results, - questions: { symptoms: {} }, + covidAoeQuestions: { symptoms: {} }, errors: { dateTested: "", deviceId: "", specimenId: "" }, }; const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); - const [hasAnySymptoms, setHasAnySymptoms] = useState(); let deviceTypeOptions = useMemo( () => @@ -244,69 +234,19 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { /> )}
-
-
- - dispatch({ - type: TestFormActionCase.UPDATE_PREGNANCY, - payload: pregnancyCode, - }) - } - buttons={pregnancyResponses} - selectedRadio={state.questions.pregnancy} - /> -
-
-
-
- setHasAnySymptoms(e)} - /> -
-
- {hasAnySymptoms === "YES" && ( - <> -
- - dispatch({ - type: TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE, - payload: e.target.value, - }) - } - > -
-
- - dispatch({ - type: TestFormActionCase.TOGGLE_SYMPTOM, - payload: e.target.value as SymptomCode, - }) - } - /> -
- + {deviceSupportsMultiplex ? ( + <> + ) : ( + { + dispatch({ + type: TestFormActionCase.UPDATE_COVID_AOE_RESPONSES, + payload: responses, + }); + }} + /> )}
diff --git a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx index d65b869b11..d1b835f77a 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx @@ -1,9 +1,6 @@ import moment from "moment/moment"; -import { - PregnancyCode, - SymptomCode, -} from "../../../patientApp/timeOfTest/constants"; +import { PregnancyCode } from "../../../patientApp/timeOfTest/constants"; import { MultiplexResultInput } from "../../../generated/graphql"; import { DevicesMap } from "../QueueItem"; @@ -13,7 +10,7 @@ export interface TestFormState { deviceId: string; specimenId: string; testResults: MultiplexResultInput[]; - questions: TestQuestionResponses; + covidAoeQuestions: CovidAoeQuestionResponses; errors: { dateTested: string; deviceId: string; @@ -21,7 +18,7 @@ export interface TestFormState { }; } -export interface TestQuestionResponses { +export interface CovidAoeQuestionResponses { pregnancy?: PregnancyCode; // SymptomInputs should probably be updated to use SymptomCode and SymptomName types symptoms: Record; @@ -34,10 +31,7 @@ export enum TestFormActionCase { UPDATE_DEVICE_ID = "UPDATE_DEVICE_ID", UPDATE_SPECIMEN_ID = "UPDATE_SPECIMEN_ID", UPDATE_TEST_RESULT = "UPDATE_TEST_RESULT", - UPDATE_PREGNANCY = "UPDATE_PREGNANCY", - TOGGLE_SYMPTOM = "TOGGLE_SYMPTOM", - UPDATE_SYMPTOMS = "UPDATE_SYMPTOMS", - UPDATE_SYMPTOM_ONSET_DATE = "UPDATE_SYMPTOM_ONSET_DATE", + UPDATE_COVID_AOE_RESPONSES = "UPDATE_COVID_AOE_RESPONSES", } export type TestFormAction = @@ -52,13 +46,10 @@ export type TestFormAction = type: TestFormActionCase.UPDATE_TEST_RESULT; payload: MultiplexResultInput[]; } - | { type: TestFormActionCase.UPDATE_PREGNANCY; payload: PregnancyCode } - | { type: TestFormActionCase.TOGGLE_SYMPTOM; payload: SymptomCode } | { - type: TestFormActionCase.UPDATE_SYMPTOMS; - payload: Record; - } - | { type: TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE; payload: string }; + type: TestFormActionCase.UPDATE_COVID_AOE_RESPONSES; + payload: CovidAoeQuestionResponses; + }; export const testCardFormReducer = ( prevState: TestFormState, @@ -117,40 +108,17 @@ export const testCardFormReducer = ( }; } case TestFormActionCase.UPDATE_TEST_RESULT: { - console.log(prevState, payload); return { ...prevState, testResults: payload, dirty: true, }; } - case TestFormActionCase.UPDATE_PREGNANCY: { - return { - ...prevState, - questions: { - ...prevState.questions, - pregnancy: payload, - }, - }; - } - case TestFormActionCase.TOGGLE_SYMPTOM: { + case TestFormActionCase.UPDATE_COVID_AOE_RESPONSES: { return { ...prevState, - questions: { - ...prevState.questions, - symptoms: { - ...prevState.questions.symptoms, - }, - }, - }; - } - case TestFormActionCase.UPDATE_SYMPTOM_ONSET_DATE: { - return { - ...prevState, - questions: { - ...prevState.questions, - symptomOnsetDate: moment(payload).toISOString(), - }, + dirty: true, + covidAoeQuestions: payload, }; } } From 6c936de8e6cd677e5b637912de4a91ff5d927aef Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 7 Sep 2023 11:09:41 -0400 Subject: [PATCH 09/78] Update symptoms and save form edits --- .../testQueue/TestCard/AoE/CovidAoEForm.tsx | 45 ++++- .../src/app/testQueue/TestCard/TestCard.tsx | 7 +- .../app/testQueue/TestCard/TestCardForm.tsx | 158 ++++++++++++++---- .../TestCard/TestCardFormReducer.tsx | 41 ++++- 4 files changed, 205 insertions(+), 46 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx index 77fa4a06fa..52fc709718 100644 --- a/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx +++ b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx @@ -10,7 +10,6 @@ import { getPregnancyResponses, globalSymptomDefinitions, PregnancyCode, - SymptomCode, } from "../../../../patientApp/timeOfTest/constants"; import { QueriedTestOrder } from "../../QueueItem"; import { CovidAoeQuestionResponses } from "../TestCardFormReducer"; @@ -23,6 +22,28 @@ export interface CovidAoEFormProps { const pregnancyResponses = getPregnancyResponses(); +const parseSymptoms = (symptomsJsonString: string | null | undefined) => { + const symptoms: Record = {}; + if (symptomsJsonString) { + const parsedSymptoms: { [key: string]: string | boolean } = + JSON.parse(symptomsJsonString); + + globalSymptomDefinitions.forEach((opt) => { + const val = opt.value; + if (typeof parsedSymptoms[val] === "string") { + symptoms[val] = parsedSymptoms[val] === "true"; + } else { + symptoms[val] = parsedSymptoms[val] as boolean; + } + }); + } else { + globalSymptomDefinitions.forEach((opt) => { + symptoms[opt.value] = false; + }); + } + return symptoms; +}; + const CovidAoEForm = ({ testOrder, responses, @@ -30,6 +51,8 @@ const CovidAoEForm = ({ }: CovidAoEFormProps) => { const [hasAnySymptoms, setHasAnySymptoms] = useState(); + const symptoms: Record = parseSymptoms(responses.symptoms); + const onPregnancyChange = (pregnancyCode: PregnancyCode) => { onResponseChange({ ...responses, pregnancy: pregnancyCode }); }; @@ -41,12 +64,16 @@ const CovidAoEForm = ({ }); }; - const onSymptomsChange = (symptom: string) => { - let updateSymptoms = { ...responses.symptoms }; - updateSymptoms[symptom] = !updateSymptoms[symptom]; + const onSymptomsChange = ( + event: React.ChangeEvent, + currentSymptoms: Record + ) => { onResponseChange({ ...responses, - symptoms: updateSymptoms, + symptoms: JSON.stringify({ + ...currentSymptoms, + [event.target.value]: event.target.checked, + }), }); }; @@ -91,10 +118,14 @@ const CovidAoEForm = ({
({ + label, + value, + checked: symptoms[value], + }))} legend="Select any symptoms the patient is experiencing" name={`symptoms-${testOrder.internalId}`} - onChange={(e) => onSymptomsChange(e.target.value as SymptomCode)} + onChange={(e) => onSymptomsChange(e, symptoms)} />
diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index c46e44b20e..c61a9eb9b1 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -34,7 +34,6 @@ export const TestCard = ({ ); const [isOpen, setIsOpen] = useState(false); - const [isCollapsible, setIsCollapsible] = useState(true); const timerContext = { organizationName: organization.name, @@ -59,11 +58,7 @@ export const TestCard = ({
-
- + ); }; diff --git a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx index d1b835f77a..5b92cf6bd4 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx @@ -2,10 +2,12 @@ import moment from "moment/moment"; import { PregnancyCode } from "../../../patientApp/timeOfTest/constants"; import { MultiplexResultInput } from "../../../generated/graphql"; -import { DevicesMap } from "../QueueItem"; +import { DevicesMap, QueriedTestOrder } from "../QueueItem"; + +import { convertFromMultiplexResponse } from "./TestCardForm"; export interface TestFormState { - dateTested?: string; + dateTested: string; dirty: boolean; deviceId: string; specimenId: string; @@ -20,8 +22,7 @@ export interface TestFormState { export interface CovidAoeQuestionResponses { pregnancy?: PregnancyCode; - // SymptomInputs should probably be updated to use SymptomCode and SymptomName types - symptoms: Record; + symptoms?: string | null; symptomOnsetDate?: string; } @@ -32,6 +33,8 @@ export enum TestFormActionCase { UPDATE_SPECIMEN_ID = "UPDATE_SPECIMEN_ID", UPDATE_TEST_RESULT = "UPDATE_TEST_RESULT", UPDATE_COVID_AOE_RESPONSES = "UPDATE_COVID_AOE_RESPONSES", + UPDATE_DIRTY_STATE = "UPDATE_DIRTY_STATE", + UPDATE_WITH_CHANGES_FROM_SERVER = "UPDATE_WITH_CHANGES_FROM_SERVER", } export type TestFormAction = @@ -49,6 +52,14 @@ export type TestFormAction = | { type: TestFormActionCase.UPDATE_COVID_AOE_RESPONSES; payload: CovidAoeQuestionResponses; + } + | { + type: TestFormActionCase.UPDATE_DIRTY_STATE; + payload: boolean; + } + | { + type: TestFormActionCase.UPDATE_WITH_CHANGES_FROM_SERVER; + payload: QueriedTestOrder; }; export const testCardFormReducer = ( @@ -121,6 +132,28 @@ export const testCardFormReducer = ( covidAoeQuestions: payload, }; } + case TestFormActionCase.UPDATE_DIRTY_STATE: { + return { + ...prevState, + dirty: false, + }; + } + case TestFormActionCase.UPDATE_WITH_CHANGES_FROM_SERVER: { + return { + ...prevState, + dirty: false, + deviceId: payload.deviceType.internalId, + specimenId: payload.specimenType.internalId, + dateTested: payload.dateTested, + testResults: convertFromMultiplexResponse(payload.results), + covidAoeQuestions: { + ...prevState.covidAoeQuestions, + symptoms: payload.symptoms, + symptomOnsetDate: payload.symptomOnset, + pregnancy: payload.pregnancy as PregnancyCode, + }, + }; + } } throw Error("Unknown action: " + type); }; From ac5f3cae1050f39db5b7fcc0a440808963019ac4 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 11 Sep 2023 13:47:16 -0400 Subject: [PATCH 10/78] Add submit and validation --- .../src/app/testQueue/TestCard/TestCard.tsx | 4 + .../app/testQueue/TestCard/TestCardForm.tsx | 133 ++++++++++++++++-- .../TestCard/TestCardFormReducer.tsx | 5 - 3 files changed, 124 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index c61a9eb9b1..35d193e0ae 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -11,18 +11,21 @@ import { TestTimerWidget, useTestTimer } from "../TestTimer"; import { RootState } from "../../store"; import TestCardForm from "./TestCardForm"; + import "./TestCard.scss"; export interface TestCardProps { testOrder: QueriedTestOrder; facility: QueriedFacility; devicesMap: DevicesMap; + refetchQueue: () => void; } export const TestCard = ({ testOrder, facility, devicesMap, + refetchQueue, }: TestCardProps) => { const navigate = useNavigate(); const timer = useTestTimer( @@ -113,6 +116,7 @@ export const TestCard = ({ testOrder={testOrder} devicesMap={devicesMap} facility={facility} + refetchQueue={refetchQueue} >
diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index 669e26b22d..810d775b43 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -1,6 +1,7 @@ import moment from "moment"; import { Alert, Button } from "@trussworks/react-uswds"; import React, { useEffect, useMemo, useReducer, useState } from "react"; +import { FetchResult } from "@apollo/client"; import TextInput from "../../commonComponents/TextInput"; import { DevicesMap, QueriedFacility, QueriedTestOrder } from "../QueueItem"; @@ -10,9 +11,15 @@ import Dropdown from "../../commonComponents/Dropdown"; import { MULTIPLEX_DISEASES } from "../../testResults/constants"; import { MultiplexResultInput, + SubmitQueueItemMutation, useEditQueueItemMutation, + useSubmitQueueItemMutation, } from "../../../generated/graphql"; -import { updateTimer } from "../TestTimer"; +import { removeTimer, updateTimer } from "../TestTimer"; +import { getAppInsights } from "../../TelemetryService"; +import { ALERT_CONTENT, QUEUE_NOTIFICATION_TYPES } from "../constants"; +import { showError, showSuccess } from "../../utils/srToast"; +import { displayFullName } from "../../utils"; import { TestFormActionCase, @@ -27,6 +34,7 @@ export interface TestFormProps { testOrder: QueriedTestOrder; devicesMap: DevicesMap; facility: QueriedFacility; + refetchQueue: () => void; } interface UpdateQueueItemProps { @@ -80,7 +88,12 @@ export const convertFromMultiplexResponse = ( })); }; -const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { +const TestCardForm = ({ + testOrder, + devicesMap, + facility, + refetchQueue, +}: TestFormProps) => { const initialFormState: TestFormState = { dirty: false, dateTested: testOrder.dateTested, @@ -88,11 +101,27 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { specimenId: testOrder.specimenType.internalId ?? "", testResults: testOrder.results, covidAoeQuestions: {}, - errors: { dateTested: "", deviceId: "", specimenId: "" }, }; const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); const [saveState, setSaveState] = useState("idle"); const [editQueueItem] = useEditQueueItemMutation(); + const [submitTestResult, { loading }] = useSubmitQueueItemMutation(); + const appInsights = getAppInsights(); + const trackRemovePatientFromQueue = () => { + if (appInsights) { + appInsights.trackEvent({ name: "Remove Patient From Queue" }); + } + }; + const trackSubmitTestResult = () => { + if (appInsights) { + appInsights.trackEvent({ name: "Submit Test Result" }); + } + }; + const trackUpdateAoEResponse = () => { + if (appInsights) { + appInsights.trackEvent({ name: "Update AoE Response" }); + } + }; const DEBOUNCE_TIME = 300; @@ -138,6 +167,29 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { return doesDeviceSupportMultiplex(state.deviceId, devicesMap); }, [devicesMap, state.deviceId]); + const validateDateTested = () => { + const EARLIEST_TEST_DATE = new Date("01/01/2020 12:00:00 AM"); + if (!state.dateTested) { + return ""; + } + const dateTested = new Date(state.dateTested); // local time, may be an invalid date + // if it is an invalid date + if (isNaN(dateTested.getTime())) { + return "Test date is invalid. Please enter using the format MM/DD/YYYY."; + } + if (state.dateTested && dateTested < EARLIEST_TEST_DATE) { + return `Test date must be after ${moment(EARLIEST_TEST_DATE).format( + "MM/DD/YYYY" + )}.`; + } + if (state.dateTested && dateTested > new Date()) { + return "Test date can't be in the future."; + } + return ""; + }; + + const dateTestedErrorMessage = validateDateTested(); + const isBeforeDateWarningThreshold = moment(state.dateTested) < moment().subtract(6, "months"); @@ -167,6 +219,7 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { .catch((e) => console.error("temp dev test, will be caught by apollo")); }; + // when user makes changes, send update to backend useEffect(() => { let debounceTimer: ReturnType; if (state.dirty) { @@ -193,11 +246,10 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { // eslint-disable-next-line }, [state.deviceId, state.specimenId, state.dateTested, state.testResults]); + // when backend sends update on test order, update the form state useEffect(() => { - if (state.dirty) { - // don't update if not done saving changes - return; - } + // don't update if not done saving changes + if (state.dirty) return; dispatch({ type: TestFormActionCase.UPDATE_WITH_CHANGES_FROM_SERVER, payload: testOrder, @@ -205,9 +257,62 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { // eslint-disable-next-line }, [testOrder]); - const onSubmit = (e: React.FormEvent) => { + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); - console.log("submit", state); + if (state.dateTested && validateDateTested().length === 0) { + showError(dateTestedErrorMessage, "Invalid test date"); + return; + } + // check force submit and confirmation type logic + + setSaveState("saving"); + if (appInsights) { + trackSubmitTestResult(); + } + try { + const result = await submitTestResult({ + variables: { + patientId: testOrder.patient?.internalId, + deviceTypeId: state.deviceId, + specimenTypeId: state.specimenId, + dateTested: state.dateTested, + results: doesDeviceSupportMultiplex(state.deviceId, devicesMap) + ? state.testResults + : state.testResults.filter( + (result) => result.diseaseName === MULTIPLEX_DISEASES.COVID_19 + ), + }, + }); + notifyUserOnResponse(result); + refetchQueue(); + removeTimer(testOrder.internalId); + } catch (error: any) { + setSaveState("error"); + } + }; + + const notifyUserOnResponse = ( + response: FetchResult + ) => { + let { title, body } = { + ...ALERT_CONTENT[QUEUE_NOTIFICATION_TYPES.SUBMITTED_RESULT__SUCCESS]( + testOrder.patient + ), + }; + + const patientFullName = displayFullName( + testOrder.patient.firstName, + testOrder.patient.middleName, + testOrder.patient.lastName + ); + + if (response?.data?.submitQueueItem?.deliverySuccess === false) { + let deliveryFailureTitle = `Unable to text result to ${patientFullName}`; + let deliveryFailureMsg = + "The phone number provided may not be valid or may not be able to accept text messages"; + showError(deliveryFailureMsg, deliveryFailureTitle); + } + showSuccess(body, title); }; return ( @@ -242,6 +347,8 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { }) } disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} + validationStatus={dateTestedErrorMessage ? "error" : undefined} + errorMessage={dateTestedErrorMessage} >
@@ -287,8 +394,8 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { } className="card-dropdown" data-testid="device-type-dropdown" - errorMessage={state.errors.deviceId} - validationStatus={state.errors.deviceId ? "error" : "success"} + errorMessage={deviceTypeIsInvalid ? "Invalid device type" : ""} + validationStatus={deviceTypeIsInvalid ? "error" : undefined} />
@@ -306,8 +413,8 @@ const TestCardForm = ({ testOrder, devicesMap, facility }: TestFormProps) => { className="card-dropdown" data-testid="specimen-type-dropdown" disabled={specimenTypeOptions.length === 0} - errorMessage={state.errors.specimenId} - validationStatus={state.errors.specimenId ? "error" : "success"} + errorMessage={specimenTypeIsInvalid ? "Invalid specimen type" : ""} + validationStatus={specimenTypeIsInvalid ? "error" : undefined} />
diff --git a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx index 5b92cf6bd4..663220cfe3 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardFormReducer.tsx @@ -13,11 +13,6 @@ export interface TestFormState { specimenId: string; testResults: MultiplexResultInput[]; covidAoeQuestions: CovidAoeQuestionResponses; - errors: { - dateTested: string; - deviceId: string; - specimenId: string; - }; } export interface CovidAoeQuestionResponses { From 73084bdd76133ec0a0cb4a059f55afbeb4ad1561 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 13 Sep 2023 13:24:42 -0400 Subject: [PATCH 11/78] Update card styling --- .../src/app/testQueue/TestCard/TestCard.scss | 25 +++++++++++++++++++ .../testQueue/TestCard/TestCard.stories.tsx | 2 ++ .../src/app/testQueue/TestCard/TestCard.tsx | 9 ++++--- .../app/testQueue/TestCard/TestCardForm.tsx | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.scss b/frontend/src/app/testQueue/TestCard/TestCard.scss index 77a57730b2..cff3cbd5af 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.scss +++ b/frontend/src/app/testQueue/TestCard/TestCard.scss @@ -1,3 +1,28 @@ +@use "../../../scss/settings" as settings; + .list-style-none { list-style: none; } + +.close-button { + color: settings.$theme-color-prime-gray-darkest; +} + +.close-button-col { + position: relative; + bottom: 0.5rem; + left: 1rem; +} + +.timer-col { + position: relative; + left: 0.5rem; +} + +.test-card-body { + border-top: 2px solid #dfe1e2; +} + +.test-card-container { + max-width: 64rem; +} diff --git a/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx index 572dc3b5a2..bf957799f8 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx @@ -31,6 +31,8 @@ const Template: StoryFn = (args: TestCardProps) => ( + + ); diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 35d193e0ae..5509c0080f 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -56,7 +56,7 @@ export const TestCard = ({ const toggleOpen = () => setIsOpen((prevState) => !prevState); return ( - +
@@ -91,11 +91,12 @@ export const TestCard = ({
-
+
-
+
- +
From 77b1e2382f159881f28b66a49361f5e3af45f8cc Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 14 Sep 2023 20:53:33 -0400 Subject: [PATCH 12/78] Revert demo changes to single card --- frontend/src/app/testQueue/TestCard/TestCard.stories.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx index bf957799f8..572dc3b5a2 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.stories.tsx @@ -31,8 +31,6 @@ const Template: StoryFn = (args: TestCardProps) => ( - - ); From 252913a6200c00e89151fea1eccb4686b1d4c637 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 14 Sep 2023 20:53:53 -0400 Subject: [PATCH 13/78] Add test date and time validation style --- .../app/testQueue/TestCard/TestCardForm.scss | 4 ++++ .../src/app/testQueue/TestCard/TestCardForm.tsx | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/testQueue/TestCard/TestCardForm.scss diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.scss b/frontend/src/app/testQueue/TestCard/TestCardForm.scss new file mode 100644 index 0000000000..4232f68ec5 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.scss @@ -0,0 +1,4 @@ +.no-left-border { + border-left-width: 0; + padding-left: 0; +} diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index b061987410..5e47221184 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -20,6 +20,7 @@ import { getAppInsights } from "../../TelemetryService"; import { ALERT_CONTENT, QUEUE_NOTIFICATION_TYPES } from "../constants"; import { showError, showSuccess } from "../../utils/srToast"; import { displayFullName } from "../../utils"; +import "./TestCardForm.scss"; import { TestFormActionCase, @@ -103,6 +104,7 @@ const TestCardForm = ({ covidAoeQuestions: {}, }; const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); + const [dateTestedTouched, setDateTestedTouched] = useState(false); const [saveState, setSaveState] = useState("idle"); const [editQueueItem] = useEditQueueItemMutation(); const [submitTestResult, { loading }] = useSubmitQueueItemMutation(); @@ -340,6 +342,7 @@ const TestCardForm = ({ min={formatDate(new Date("Jan 1, 2020"))} max={formatDate(moment().toDate())} value={formatDate(moment(state.dateTested).toDate())} + onBlur={(e) => setDateTestedTouched(true)} onChange={(e) => dispatch({ type: TestFormActionCase.UPDATE_DATE_TESTED, @@ -347,13 +350,15 @@ const TestCardForm = ({ }) } disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} - validationStatus={dateTestedErrorMessage ? "error" : undefined} - errorMessage={dateTestedErrorMessage} + validationStatus={ + dateTestedTouched && dateTestedErrorMessage ? "error" : undefined + } + errorMessage={dateTestedTouched && dateTestedErrorMessage} >
-
+
setDateTestedTouched(true)} + validationStatus={ + dateTestedTouched && dateTestedErrorMessage ? "error" : undefined + } disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} >
From 1657d2fc97a86061c6008881d146a45db1dae6f5 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 14 Sep 2023 20:58:34 -0400 Subject: [PATCH 14/78] Update test result radio group style --- .../TestCard/CovidResultInputGroup.tsx | 52 +++++++++--------- .../TestCard/MultiplexResultInputGroup.tsx | 18 +++---- .../app/testQueue/TestCard/TestCardForm.tsx | 54 ++++++++++--------- 3 files changed, 58 insertions(+), 66 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/CovidResultInputGroup.tsx b/frontend/src/app/testQueue/TestCard/CovidResultInputGroup.tsx index d0d31eed65..403052c0f4 100644 --- a/frontend/src/app/testQueue/TestCard/CovidResultInputGroup.tsx +++ b/frontend/src/app/testQueue/TestCard/CovidResultInputGroup.tsx @@ -56,34 +56,30 @@ const CovidResultInputGroup: React.FC = ({ }; return ( -
-

COVID-19 results

- { - convertAndSendResults(value as TestResult); - }} - buttons={[ - { - value: COVID_RESULTS.POSITIVE, - label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, - }, - { - value: COVID_RESULTS.NEGATIVE, - label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, - }, - { - value: COVID_RESULTS.INCONCLUSIVE, - label: `${TEST_RESULT_DESCRIPTIONS.UNDETERMINED}`, - }, - ]} - name={`covid-test-result-${queueItemId}`} - selectedRadio={resultCovidFormat} - wrapperClassName="prime-radio__group" - disabled={isSubmitDisabled} - /> - + { + convertAndSendResults(value as TestResult); + }} + buttons={[ + { + value: COVID_RESULTS.POSITIVE, + label: `${TEST_RESULT_DESCRIPTIONS.POSITIVE} (+)`, + }, + { + value: COVID_RESULTS.NEGATIVE, + label: `${TEST_RESULT_DESCRIPTIONS.NEGATIVE} (-)`, + }, + { + value: COVID_RESULTS.INCONCLUSIVE, + label: `${TEST_RESULT_DESCRIPTIONS.UNDETERMINED}`, + }, + ]} + name={`covid-test-result-${queueItemId}`} + selectedRadio={resultCovidFormat} + wrapperClassName="prime-radio__group" + disabled={isSubmitDisabled} + /> ); }; diff --git a/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx index 1ee471be0a..e6dea465da 100644 --- a/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx +++ b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx @@ -256,17 +256,15 @@ const MultiplexResultInputGroup: React.FC = ({ }; return ( -
-
+ <> +
{!isFluOnly && (
-

COVID-19

{ setMultiplexResultInput("covid", value); }} @@ -287,13 +285,11 @@ const MultiplexResultInputGroup: React.FC = ({
)}
-

Flu A

{ setMultiplexResultInput("fluA", value); }} @@ -313,13 +309,11 @@ const MultiplexResultInputGroup: React.FC = ({ />
-

Flu B

{ setMultiplexResultInput("fluB", value); }} @@ -368,7 +362,7 @@ const MultiplexResultInputGroup: React.FC = ({ />
- + ); }; diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index 5e47221184..173405df2e 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -1,5 +1,5 @@ import moment from "moment"; -import { Alert, Button } from "@trussworks/react-uswds"; +import { Alert, Button, FormGroup } from "@trussworks/react-uswds"; import React, { useEffect, useMemo, useReducer, useState } from "react"; import { FetchResult } from "@apollo/client"; @@ -428,31 +428,33 @@ const TestCardForm = ({
- {deviceSupportsMultiplex ? ( - - dispatch({ - type: TestFormActionCase.UPDATE_TEST_RESULT, - payload: results, - }) - } - > - ) : ( - - dispatch({ - type: TestFormActionCase.UPDATE_TEST_RESULT, - payload: results, - }) - } - /> - )} + + {deviceSupportsMultiplex ? ( + + dispatch({ + type: TestFormActionCase.UPDATE_TEST_RESULT, + payload: results, + }) + } + > + ) : ( + + dispatch({ + type: TestFormActionCase.UPDATE_TEST_RESULT, + payload: results, + }) + } + /> + )} +
Date: Mon, 18 Sep 2023 13:47:13 -0400 Subject: [PATCH 15/78] Update card styling, add submit loader, correction alert --- .../testQueue/TestCard/AoE/CovidAoEForm.tsx | 98 ++-- .../TestCard/MultiplexResultInputGroup.tsx | 119 ++--- .../src/app/testQueue/TestCard/TestCard.scss | 20 +- .../src/app/testQueue/TestCard/TestCard.tsx | 119 ++--- .../app/testQueue/TestCard/TestCardForm.scss | 4 +- .../app/testQueue/TestCard/TestCardForm.tsx | 442 +++++++++++------- .../TestCard/TestCardSubmitLoader.scss | 37 ++ .../TestCard/TestCardSubmitLoader.tsx | 31 ++ frontend/src/app/testQueue/TestQueue.tsx | 23 +- 9 files changed, 545 insertions(+), 348 deletions(-) create mode 100644 frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.scss create mode 100644 frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.tsx diff --git a/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx index 52fc709718..ad070bba02 100644 --- a/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx +++ b/frontend/src/app/testQueue/TestCard/AoE/CovidAoEForm.tsx @@ -79,57 +79,59 @@ const CovidAoEForm = ({ return ( <> -
-
- -
-
-
-
- setHasAnySymptoms(e)} - /> -
-
- {hasAnySymptoms === "YES" && ( - <> -
- onSymptomOnsetDateChange(e.target.value)} - > +
+
+
+
-
- ({ - label, - value, - checked: symptoms[value], - }))} - legend="Select any symptoms the patient is experiencing" - name={`symptoms-${testOrder.internalId}`} - onChange={(e) => onSymptomsChange(e, symptoms)} +
+
+
+ setHasAnySymptoms(e)} />
- - )} +
+ {hasAnySymptoms === "YES" && ( + <> +
+ onSymptomOnsetDateChange(e.target.value)} + > +
+
+ ({ + label, + value, + checked: symptoms[value], + }))} + legend="Select any symptoms the patient is experiencing" + name={`symptoms-${testOrder.internalId}`} + onChange={(e) => onSymptomsChange(e, symptoms)} + /> +
+ + )} +
); }; diff --git a/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx index e6dea465da..4da6a20342 100644 --- a/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx +++ b/frontend/src/app/testQueue/TestCard/MultiplexResultInputGroup.tsx @@ -26,10 +26,10 @@ interface MultiplexResultState { fluB: TestResult; } -const convertFromMultiplexResultInputs = ( +export const convertFromMultiplexResultInputs = ( diseaseResults: MultiplexResultInput[] ): MultiplexResultState => { - const multiplexResult: MultiplexResultState = { + return { covid: (findResultByDiseaseName( diseaseResults ?? [], @@ -46,8 +46,6 @@ const convertFromMultiplexResultInputs = ( MULTIPLEX_DISEASES.FLU_B ) as TestResult) ?? TEST_RESULTS.UNKNOWN, }; - - return multiplexResult; }; const convertFromMultiplexResult = ( @@ -119,6 +117,66 @@ const doesDeviceSupportMultiplexAndCovidOnlyResult = ( return false; }; +export const validateMultiplexResultState = ( + resultsMultiplexFormat: MultiplexResultState, + deviceId: string, + devicesMap: DevicesMap +) => { + const deviceSupportsCovidOnlyResult = + doesDeviceSupportMultiplexAndCovidOnlyResult(deviceId, devicesMap); + const isFluOnly = isDeviceFluOnly(deviceId, devicesMap); + + let allResultsAreInconclusive = + resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + + let anyResultIsInconclusive = + resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + + let allResultsAreEqual = + resultsMultiplexFormat.covid === resultsMultiplexFormat.fluA && + resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; + + if (isFluOnly) { + allResultsAreEqual = + resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; + allResultsAreInconclusive = + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED && + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED; + anyResultIsInconclusive = + resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || + resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; + } + + const covidIsFilled = + resultsMultiplexFormat.covid === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.covid === TEST_RESULTS.NEGATIVE; + + const fluAIsFilled = + resultsMultiplexFormat.fluA === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.fluA === TEST_RESULTS.NEGATIVE; + + const fluBIsFilled = + resultsMultiplexFormat.fluB === TEST_RESULTS.POSITIVE || + resultsMultiplexFormat.fluB === TEST_RESULTS.NEGATIVE; + + if (anyResultIsInconclusive && !allResultsAreEqual) { + return false; + } + return ( + allResultsAreInconclusive || + (deviceSupportsCovidOnlyResult && + covidIsFilled && + !fluAIsFilled && + !fluBIsFilled) || + (isFluOnly && fluAIsFilled && fluBIsFilled) || + (covidIsFilled && fluAIsFilled && fluBIsFilled) + ); +}; + /** * COMPONENT */ @@ -141,17 +199,15 @@ const MultiplexResultInputGroup: React.FC = ({ const isMobile = screen.width <= 600; const resultsMultiplexFormat: MultiplexResultState = convertFromMultiplexResultInputs(testResults); - let inconclusiveCheck = + let allResultsInconclusive = resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED && resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED && resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; - const deviceSupportsCovidOnlyResult = - doesDeviceSupportMultiplexAndCovidOnlyResult(deviceId, devicesMap); const isFluOnly = isDeviceFluOnly(deviceId, devicesMap); if (isFluOnly) { - inconclusiveCheck = + allResultsInconclusive = resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED && resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED; } @@ -164,7 +220,7 @@ const MultiplexResultInputGroup: React.FC = ({ value: TestResult ) => { let newResults: MultiplexResultState = resultsMultiplexFormat; - if (inconclusiveCheck) { + if (allResultsInconclusive) { newResults = { covid: TEST_RESULTS.UNKNOWN, fluA: TEST_RESULTS.UNKNOWN, @@ -211,49 +267,6 @@ const MultiplexResultInputGroup: React.FC = ({ /** * Form Validation * */ - const validateForm = () => { - let anyResultIsInconclusive = - resultsMultiplexFormat.covid === TEST_RESULTS.UNDETERMINED || - resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || - resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; - - let allResultsAreEqual = - resultsMultiplexFormat.covid === resultsMultiplexFormat.fluA && - resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; - - if (isFluOnly) { - allResultsAreEqual = - resultsMultiplexFormat.fluA === resultsMultiplexFormat.fluB; - anyResultIsInconclusive = - resultsMultiplexFormat.fluA === TEST_RESULTS.UNDETERMINED || - resultsMultiplexFormat.fluB === TEST_RESULTS.UNDETERMINED; - } - - const covidIsFilled = - resultsMultiplexFormat.covid === TEST_RESULTS.POSITIVE || - resultsMultiplexFormat.covid === TEST_RESULTS.NEGATIVE; - - const fluAIsFilled = - resultsMultiplexFormat.fluA === TEST_RESULTS.POSITIVE || - resultsMultiplexFormat.fluA === TEST_RESULTS.NEGATIVE; - - const fluBIsFilled = - resultsMultiplexFormat.fluB === TEST_RESULTS.POSITIVE || - resultsMultiplexFormat.fluB === TEST_RESULTS.NEGATIVE; - - if (anyResultIsInconclusive && !allResultsAreEqual) { - return false; - } - return ( - inconclusiveCheck || - (deviceSupportsCovidOnlyResult && - covidIsFilled && - !fluAIsFilled && - !fluBIsFilled) || - (isFluOnly && fluAIsFilled && fluBIsFilled) || - (covidIsFilled && fluAIsFilled && fluBIsFilled) - ); - }; return ( <> @@ -344,7 +357,7 @@ const MultiplexResultInputGroup: React.FC = ({ { value: "inconclusive", label: "Mark test as inconclusive", - checked: inconclusiveCheck, + checked: allResultsInconclusive, }, ]} /> diff --git a/frontend/src/app/testQueue/TestCard/TestCard.scss b/frontend/src/app/testQueue/TestCard/TestCard.scss index cff3cbd5af..dbfae028bd 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.scss +++ b/frontend/src/app/testQueue/TestCard/TestCard.scss @@ -8,21 +8,15 @@ color: settings.$theme-color-prime-gray-darkest; } -.close-button-col { - position: relative; - bottom: 0.5rem; - left: 1rem; -} - -.timer-col { - position: relative; - left: 0.5rem; -} - -.test-card-body { - border-top: 2px solid #dfe1e2; +.test-card-header-bottom-border { + border-bottom: 2px solid #dfe1e2; } .test-card-container { max-width: 64rem; } + +.usa-card__container { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 5509c0080f..27e15b3954 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -14,6 +14,8 @@ import TestCardForm from "./TestCardForm"; import "./TestCard.scss"; +export type SaveStatus = "idle" | "editing" | "saving" | "error"; + export interface TestCardProps { testOrder: QueriedTestOrder; facility: QueriedFacility; @@ -28,15 +30,12 @@ export const TestCard = ({ refetchQueue, }: TestCardProps) => { const navigate = useNavigate(); - const timer = useTestTimer( - testOrder.internalId, - testOrder.deviceType.testLength - ); + const timer = useTestTimer(testOrder.internalId, 0.5); const organization = useSelector( (state: any) => state.organization as Organization ); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(true); const timerContext = { organizationName: organization.name, @@ -57,70 +56,72 @@ export const TestCard = ({ return ( - -
-
-
- -
-
- -
-
- - DOB: {patientDateOfBirth.format("MM/DD/YYYY")} + +
+
+ +
+
+
-
-
- -
-
- -
+ +
+
+ + DOB: {patientDateOfBirth.format("MM/DD/YYYY")} + +
+
+
+ +
+
+
- -
+
+ -
- + +
); }; diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.scss b/frontend/src/app/testQueue/TestCard/TestCardForm.scss index 4232f68ec5..0c23028419 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.scss +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.scss @@ -1,4 +1,4 @@ .no-left-border { - border-left-width: 0; - padding-left: 0; + border-left-width: 0 !important; + padding-left: 0 !important; } diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index 173405df2e..c27bed2f3c 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -1,5 +1,11 @@ import moment from "moment"; -import { Alert, Button, FormGroup } from "@trussworks/react-uswds"; +import { + Alert, + Button, + Checkbox, + FormGroup, + Label, +} from "@trussworks/react-uswds"; import React, { useEffect, useMemo, useReducer, useState } from "react"; import { FetchResult } from "@apollo/client"; @@ -22,14 +28,24 @@ import { showError, showSuccess } from "../../utils/srToast"; import { displayFullName } from "../../utils"; import "./TestCardForm.scss"; +import { + TestCorrectionReason, + TestCorrectionReasons, +} from "../../testResults/TestResultCorrectionModal"; + import { TestFormActionCase, TestFormState, testCardFormReducer, } from "./TestCardFormReducer"; import CovidResultInputGroup from "./CovidResultInputGroup"; -import MultiplexResultInputGroup from "./MultiplexResultInputGroup"; +import MultiplexResultInputGroup, { + convertFromMultiplexResultInputs, + validateMultiplexResultState, +} from "./MultiplexResultInputGroup"; import CovidAoEForm from "./AoE/CovidAoEForm"; +import { SaveStatus } from "./TestCard"; +import { TestCardSubmitLoader } from "./TestCardSubmitLoader"; export interface TestFormProps { testOrder: QueriedTestOrder; @@ -78,8 +94,6 @@ const doesDeviceSupportMultiplex = ( return false; }; -export type SaveState = "idle" | "editing" | "saving" | "error"; - export const convertFromMultiplexResponse = ( responseResult: QueriedTestOrder["results"] ): MultiplexResultInput[] => { @@ -105,9 +119,11 @@ const TestCardForm = ({ }; const [state, dispatch] = useReducer(testCardFormReducer, initialFormState); const [dateTestedTouched, setDateTestedTouched] = useState(false); - const [saveState, setSaveState] = useState("idle"); + const [saveStatus, setSaveStatus] = useState("idle"); + const [testResultsError, setTestResultsError] = useState(""); const [editQueueItem] = useEditQueueItemMutation(); const [submitTestResult, { loading }] = useSubmitQueueItemMutation(); + const [useCurrentTime, setUseCurrentTime] = useState(false); const appInsights = getAppInsights(); const trackRemovePatientFromQueue = () => { if (appInsights) { @@ -190,10 +206,7 @@ const TestCardForm = ({ return ""; }; - const dateTestedErrorMessage = validateDateTested(); - - const isBeforeDateWarningThreshold = - moment(state.dateTested) < moment().subtract(6, "months"); + let dateTestedErrorMessage = validateDateTested(); const updateQueueItem = (props: UpdateQueueItemProps) => { return editQueueItem({ @@ -226,7 +239,7 @@ const TestCardForm = ({ let debounceTimer: ReturnType; if (state.dirty) { dispatch({ type: TestFormActionCase.UPDATE_DIRTY_STATE, payload: false }); - setSaveState("editing"); + setSaveStatus("editing"); debounceTimer = setTimeout(async () => { await updateQueueItem({ deviceId: state.deviceId, @@ -238,12 +251,12 @@ const TestCardForm = ({ (result) => result.diseaseName === MULTIPLEX_DISEASES.COVID_19 ), }); - setSaveState("idle"); + setSaveStatus("idle"); }, DEBOUNCE_TIME); } return () => { clearTimeout(debounceTimer); - setSaveState("idle"); + setSaveStatus("idle"); }; // eslint-disable-next-line }, [state.deviceId, state.specimenId, state.dateTested, state.testResults]); @@ -259,15 +272,41 @@ const TestCardForm = ({ // eslint-disable-next-line }, [testOrder]); + const validateForm = () => { + dateTestedErrorMessage = validateDateTested(); + setTestResultsError(""); + if (state.dateTested && dateTestedErrorMessage.length === 0) { + showError(dateTestedErrorMessage, "Invalid test date"); + return false; + } + if (deviceSupportsMultiplex) { + const multiplexResults = convertFromMultiplexResultInputs( + state.testResults + ); + const isMultiplexValid = validateMultiplexResultState( + multiplexResults, + state.deviceId, + devicesMap + ); + if (!isMultiplexValid) { + // TODO: provide better error message based on device rules + const multiplexErrorMessage = "Multiplex result is not valid"; + setTestResultsError(multiplexErrorMessage); + showError(multiplexErrorMessage, "Invalid test results"); + return false; + } + } + return true; + }; + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (state.dateTested && validateDateTested().length === 0) { - showError(dateTestedErrorMessage, "Invalid test date"); + if (!validateForm()) { return; } // check force submit and confirmation type logic - setSaveState("saving"); + setSaveStatus("saving"); if (appInsights) { trackSubmitTestResult(); } @@ -289,7 +328,7 @@ const TestCardForm = ({ refetchQueue(); removeTimer(testOrder.internalId); } catch (error: any) { - setSaveState("error"); + setSaveStatus("error"); } }; @@ -317,161 +356,234 @@ const TestCardForm = ({ showSuccess(body, title); }; + const onUseCurrentDateTime = (event: React.ChangeEvent) => { + setUseCurrentTime(event.target.checked); + dispatch({ + type: TestFormActionCase.UPDATE_DATE_TESTED, + payload: event.target.checked ? "" : moment().toISOString(), + }); + }; + + const patientFullName = displayFullName( + testOrder.patient.firstName, + testOrder.patient.middleName, + testOrder.patient.lastName + ); + + const isCorrection = testOrder.correctionStatus === "CORRECTED"; + const reasonForCorrection = + testOrder.reasonForCorrection as TestCorrectionReason; + + const correctionWarningAlert = ( + + Correction: + {reasonForCorrection in TestCorrectionReasons + ? TestCorrectionReasons[reasonForCorrection] + : reasonForCorrection} + + ); + + const showDateMonthsAgoWarning = + moment(state.dateTested) < moment().subtract(6, "months") && + dateTestedErrorMessage.length === 0; + const dateMonthsAgoWarningAlert = ( + + Check test date: The date you selected is more than six + months ago. Please make sure it's correct before submitting. + + ); + + const errorSummaryAlert = ( + +
{dateTestedErrorMessage}
+
{testResultsError}
+
+ ); + return ( -
- {isBeforeDateWarningThreshold && ( -
-
- - Check test date: The date you selected is more - than six months ago. Please make sure it's correct before - submitting. - + <> + +
+ + {isCorrection && correctionWarningAlert} + {showDateMonthsAgoWarning && dateMonthsAgoWarningAlert} + {dateTestedErrorMessage && testResultsError && errorSummaryAlert} + {!useCurrentTime && ( +
+
+ setDateTestedTouched(true)} + onChange={(e) => + dispatch({ + type: TestFormActionCase.UPDATE_DATE_TESTED, + payload: e.target.value, + }) + } + disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} + validationStatus={ + dateTestedTouched && dateTestedErrorMessage + ? "error" + : undefined + } + errorMessage={dateTestedTouched && dateTestedErrorMessage} + > +
+
+ + dispatch({ + type: TestFormActionCase.UPDATE_TIME_TESTED, + payload: e.target.value, + }) + } + onBlur={(e) => setDateTestedTouched(true)} + validationStatus={ + dateTestedTouched && dateTestedErrorMessage + ? "error" + : undefined + } + disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} + > +
+
+ )} +
+
+ {useCurrentTime && ( + + )} + +
-
- )} -
-
- setDateTestedTouched(true)} - onChange={(e) => - dispatch({ - type: TestFormActionCase.UPDATE_DATE_TESTED, - payload: e.target.value, - }) - } - disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} - validationStatus={ - dateTestedTouched && dateTestedErrorMessage ? "error" : undefined - } - errorMessage={dateTestedTouched && dateTestedErrorMessage} - > -
-
- - dispatch({ - type: TestFormActionCase.UPDATE_TIME_TESTED, - payload: e.target.value, - }) - } - onBlur={(e) => setDateTestedTouched(true)} - validationStatus={ - dateTestedTouched && dateTestedErrorMessage ? "error" : undefined - } - disabled={deviceTypeIsInvalid || specimenTypeIsInvalid} - > -
-
-
-
- - +
+ + + + } + name="testDevice" + selectedValue={state.deviceId} + onChange={(e) => + dispatch({ + type: TestFormActionCase.UPDATE_DEVICE_ID, + payload: { deviceId: e.target.value, devicesMap }, + }) + } + className="card-dropdown" + data-testid="device-type-dropdown" + errorMessage={deviceTypeIsInvalid ? "Invalid device type" : ""} + validationStatus={deviceTypeIsInvalid ? "error" : undefined} + /> +
+
+ + dispatch({ + type: TestFormActionCase.UPDATE_SPECIMEN_ID, + payload: e.target.value, + }) + } + className="card-dropdown" + data-testid="specimen-type-dropdown" + disabled={specimenTypeOptions.length === 0} + errorMessage={ + specimenTypeIsInvalid ? "Invalid specimen type" : "" + } + validationStatus={specimenTypeIsInvalid ? "error" : undefined} + /> +
+
+
+ + {deviceSupportsMultiplex ? ( + + dispatch({ + type: TestFormActionCase.UPDATE_TEST_RESULT, + payload: results, + }) + } + > + ) : ( + + dispatch({ + type: TestFormActionCase.UPDATE_TEST_RESULT, + payload: results, + }) + } /> - - } - name="testDevice" - selectedValue={state.deviceId} - onChange={(e) => - dispatch({ - type: TestFormActionCase.UPDATE_DEVICE_ID, - payload: { deviceId: e.target.value, devicesMap }, - }) - } - className="card-dropdown" - data-testid="device-type-dropdown" - errorMessage={deviceTypeIsInvalid ? "Invalid device type" : ""} - validationStatus={deviceTypeIsInvalid ? "error" : undefined} - /> -
-
- - dispatch({ - type: TestFormActionCase.UPDATE_SPECIMEN_ID, - payload: e.target.value, - }) - } - className="card-dropdown" - data-testid="specimen-type-dropdown" - disabled={specimenTypeOptions.length === 0} - errorMessage={specimenTypeIsInvalid ? "Invalid specimen type" : ""} - validationStatus={specimenTypeIsInvalid ? "error" : undefined} - /> -
-
-
- - {deviceSupportsMultiplex ? ( - - dispatch({ - type: TestFormActionCase.UPDATE_TEST_RESULT, - payload: results, - }) - } - > - ) : ( - + )} + +
+
+ { dispatch({ - type: TestFormActionCase.UPDATE_TEST_RESULT, - payload: results, - }) - } + type: TestFormActionCase.UPDATE_COVID_AOE_RESPONSES, + payload: responses, + }); + }} /> - )} - -
- { - dispatch({ - type: TestFormActionCase.UPDATE_COVID_AOE_RESPONSES, - payload: responses, - }); - }} - /> -
-
- -
+
+
+
+ +
+
+
- + ); }; diff --git a/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.scss b/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.scss new file mode 100644 index 0000000000..bdd24f931a --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.scss @@ -0,0 +1,37 @@ +.test-card-submit-loader { + background-color: rgba(255, 255, 255, 0.92); + display: none; + top: 0; + left: 0; + border-bottom-left-radius: 0.5rem !important; + border-bottom-right-radius: 0.5rem !important; + + &-enter { + display: block; + opacity: 0; + } + + &-enter-active { + display: block; + opacity: 1; + transition: opacity 300ms; + } + + &-enter-done { + display: block; + } + + &-exit { + display: block; + opacity: 1; + } + + &-exit-active { + opacity: 0; + transition: opacity 300ms; + } + + &-exit-done { + display: none; + } +} diff --git a/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.tsx b/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.tsx new file mode 100644 index 0000000000..550c352883 --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/TestCardSubmitLoader.tsx @@ -0,0 +1,31 @@ +import classNames from "classnames"; +import { CSSTransition } from "react-transition-group"; + +import iconLoader from "../../../img/loader.svg"; + +import "./TestCardSubmitLoader.scss"; + +type Props = { + name: string; + show: boolean; +}; + +export const TestCardSubmitLoader = ({ name, show }: Props) => { + const classnames = classNames( + "test-card-submit-loader", + "z-top", + "position-absolute", + "height-full", + "width-full", + "text-center" + ); + + return ( + +
+

Submitting test data for {name}...

+ submitting +
+
+ ); +}; diff --git a/frontend/src/app/testQueue/TestQueue.tsx b/frontend/src/app/testQueue/TestQueue.tsx index e1d0087ebf..76960aab70 100644 --- a/frontend/src/app/testQueue/TestQueue.tsx +++ b/frontend/src/app/testQueue/TestQueue.tsx @@ -15,8 +15,9 @@ import { import AddToQueueSearch, { StartTestProps, } from "./addToQueue/AddToQueueSearch"; -import QueueItem, { DevicesMap } from "./QueueItem"; +import { DevicesMap } from "./QueueItem"; import "./TestQueue.scss"; +import { TestCard } from "./TestCard/TestCard"; const pollInterval = 10_000; @@ -153,14 +154,20 @@ const TestQueue: React.FC = ({ activeFacilityId }) => { onExiting={onExiting} timeout={transitionDuration} > - */} + + facility={facility} + refetchQueue={refetch} + > ); }); From b2a4520ec4f688239ba1164a708896b3b5f256fd Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 18 Sep 2023 13:47:42 -0400 Subject: [PATCH 16/78] Update timer styling --- frontend/src/app/testQueue/TestTimer.scss | 3 ++- frontend/src/app/testQueue/TestTimer.tsx | 25 ++++++++--------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/testQueue/TestTimer.scss b/frontend/src/app/testQueue/TestTimer.scss index 3e699380d2..b6f5fdaf67 100644 --- a/frontend/src/app/testQueue/TestTimer.scss +++ b/frontend/src/app/testQueue/TestTimer.scss @@ -28,6 +28,7 @@ } .timer-overtime { - color: gray; + color: settings.$color-white-transparent-50; font-style: italic; + min-width: 6rem; } diff --git a/frontend/src/app/testQueue/TestTimer.tsx b/frontend/src/app/testQueue/TestTimer.tsx index cd4288f341..4d08eb6e7b 100644 --- a/frontend/src/app/testQueue/TestTimer.tsx +++ b/frontend/src/app/testQueue/TestTimer.tsx @@ -4,8 +4,6 @@ import { faStopwatch, faRedo } from "@fortawesome/free-solid-svg-icons"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import "./TestTimer.scss"; -import { Button } from "@trussworks/react-uswds"; - import { getAppInsights } from "../../app/TelemetryService"; const alarmModule = require("./test-timer.mp3"); @@ -238,22 +236,15 @@ export const TestTimerWidget = ({ timer, context }: Props) => { if (!running) { return ( - + + {mmss(countdown)}{" "} + ); } if (countdown >= 0) { @@ -264,8 +255,8 @@ export const TestTimerWidget = ({ timer, context }: Props) => { data-testid="timer" aria-label="Reset timer" > - {mmss(countdown)}{" "} + {mmss(countdown)}{" "} ); } @@ -285,7 +276,7 @@ export const TestTimerWidget = ({ timer, context }: Props) => { aria-label="Reset timer" > RESULT READY{" "} - + {mmss(elapsed)} elapsed{" "} {" "} From 1bfde30d821d8c69f76e21a339019853aa0fd612 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 18 Sep 2023 13:52:15 -0400 Subject: [PATCH 17/78] Fix lint warnings in timer --- frontend/src/app/testQueue/TestTimer.scss | 1 - frontend/src/app/testQueue/TestTimer.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/testQueue/TestTimer.scss b/frontend/src/app/testQueue/TestTimer.scss index b6f5fdaf67..b7ed14eeb1 100644 --- a/frontend/src/app/testQueue/TestTimer.scss +++ b/frontend/src/app/testQueue/TestTimer.scss @@ -5,7 +5,6 @@ min-width: 80px; cursor: pointer; padding: 0.25rem; - padding-bottom: 0.25rem !important; display: flex; align-items: center; justify-content: space-around; diff --git a/frontend/src/app/testQueue/TestTimer.tsx b/frontend/src/app/testQueue/TestTimer.tsx index 4d08eb6e7b..aff64555e9 100644 --- a/frontend/src/app/testQueue/TestTimer.tsx +++ b/frontend/src/app/testQueue/TestTimer.tsx @@ -4,7 +4,7 @@ import { faStopwatch, faRedo } from "@fortawesome/free-solid-svg-icons"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import "./TestTimer.scss"; -import { getAppInsights } from "../../app/TelemetryService"; +import { getAppInsights } from "../TelemetryService"; const alarmModule = require("./test-timer.mp3"); From 16caa1006db1c0e002815675d6083f434345fde7 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 19 Sep 2023 10:35:56 -0400 Subject: [PATCH 18/78] Revert timer and close button style change --- frontend/src/app/testQueue/TestCard/TestCard.scss | 15 +++++++++++++-- frontend/src/app/testQueue/TestCard/TestCard.tsx | 11 +++++++---- .../src/app/testQueue/TestCard/TestCardForm.tsx | 13 ++++++------- frontend/src/app/testQueue/TestTimer.tsx | 4 +++- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCard.scss b/frontend/src/app/testQueue/TestCard/TestCard.scss index dbfae028bd..25497d238a 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.scss +++ b/frontend/src/app/testQueue/TestCard/TestCard.scss @@ -4,8 +4,19 @@ list-style: none; } -.close-button { - color: settings.$theme-color-prime-gray-darkest; +.card-close-button { + color: settings.$theme-color-prime-gray-darkest !important; +} + +.close-button-col { + position: relative; + bottom: 0.5rem; + left: 0.25rem; +} + +.timer-col { + position: relative; + right: 1rem; } .test-card-header-bottom-border { diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 27e15b3954..85b53dff52 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -30,7 +30,10 @@ export const TestCard = ({ refetchQueue, }: TestCardProps) => { const navigate = useNavigate(); - const timer = useTestTimer(testOrder.internalId, 0.5); + const timer = useTestTimer( + testOrder.internalId, + testOrder.deviceType.testLength + ); const organization = useSelector( (state: any) => state.organization as Organization ); @@ -96,9 +99,9 @@ export const TestCard = ({
-
+
- + + Correction: {reasonForCorrection in TestCorrectionReasons ? TestCorrectionReasons[reasonForCorrection] @@ -387,18 +387,17 @@ const TestCardForm = ({ moment(state.dateTested) < moment().subtract(6, "months") && dateTestedErrorMessage.length === 0; const dateMonthsAgoWarningAlert = ( - + Check test date: The date you selected is more than six months ago. Please make sure it's correct before submitting. ); const errorSummaryAlert = ( - + +
+ Please correct the following errors: +
{dateTestedErrorMessage}
{testResultsError}
diff --git a/frontend/src/app/testQueue/TestTimer.tsx b/frontend/src/app/testQueue/TestTimer.tsx index aff64555e9..419e6317dc 100644 --- a/frontend/src/app/testQueue/TestTimer.tsx +++ b/frontend/src/app/testQueue/TestTimer.tsx @@ -243,7 +243,9 @@ export const TestTimerWidget = ({ timer, context }: Props) => { aria-label="Start timer" > - {mmss(countdown)}{" "} + + Start timer + {" "} ); } From cecfece44c64cef234575baf54b929cef8dd4531 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 19 Sep 2023 11:40:30 -0400 Subject: [PATCH 19/78] Fix error summary display logic --- frontend/src/app/testQueue/TestCard/TestCardForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index 68e73a91ce..d48056eecb 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -393,6 +393,8 @@ const TestCardForm = ({
); + const showErrorSummary = + dateTestedErrorMessage.length > 0 || testResultsError.length > 0; const errorSummaryAlert = (
@@ -413,7 +415,7 @@ const TestCardForm = ({
{isCorrection && correctionWarningAlert} {showDateMonthsAgoWarning && dateMonthsAgoWarningAlert} - {dateTestedErrorMessage && testResultsError && errorSummaryAlert} + {showErrorSummary && errorSummaryAlert} {!useCurrentTime && (
From 49f5a5565a92faf9cf874ad69be957aaae6e88fc Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 19 Sep 2023 11:50:17 -0400 Subject: [PATCH 20/78] Add unordered list for error summary --- frontend/src/app/testQueue/TestCard/TestCardForm.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index d48056eecb..ae43a365b3 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -400,8 +400,10 @@ const TestCardForm = ({
Please correct the following errors:
-
{dateTestedErrorMessage}
-
{testResultsError}
+
    + {dateTestedErrorMessage &&
  • {dateTestedErrorMessage}
  • } + {testResultsError &&
  • {testResultsError}
  • } +
); From ef1adc6b1cd7fb65d9c8a444f26334f137d60302 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 19 Sep 2023 15:13:43 -0400 Subject: [PATCH 21/78] Add submit modal --- .../app/testQueue/TestCard/TestCardForm.tsx | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx index ae43a365b3..eca28d71c1 100644 --- a/frontend/src/app/testQueue/TestCard/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCardForm.tsx @@ -2,11 +2,17 @@ import moment from "moment"; import { Alert, Button, + ButtonGroup, Checkbox, FormGroup, Label, + Modal, + ModalFooter, + ModalHeading, + ModalRef, + ModalToggleButton, } from "@trussworks/react-uswds"; -import React, { useEffect, useMemo, useReducer, useState } from "react"; +import React, { useEffect, useMemo, useReducer, useRef, useState } from "react"; import { FetchResult } from "@apollo/client"; import TextInput from "../../commonComponents/TextInput"; @@ -125,6 +131,8 @@ const TestCardForm = ({ const [submitTestResult, { loading }] = useSubmitQueueItemMutation(); const [useCurrentTime, setUseCurrentTime] = useState(false); const appInsights = getAppInsights(); + const submitModalRef = useRef(null); + const trackRemovePatientFromQueue = () => { if (appInsights) { appInsights.trackEvent({ name: "Remove Patient From Queue" }); @@ -299,12 +307,18 @@ const TestCardForm = ({ return true; }; - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const submitForm = async (forceSubmit: boolean = false) => { if (!validateForm()) { return; } // check force submit and confirmation type logic + // TODO: determine whether AOE form is valid + const areAoEAnswersComplete = false; + + if (!forceSubmit && !areAoEAnswersComplete) { + submitModalRef.current?.toggleModal(); + return; + } setSaveStatus("saving"); if (appInsights) { @@ -413,8 +427,37 @@ const TestCardForm = ({ show={saveStatus === "saving"} name={patientFullName} /> + + + The test questionnaire for {patientFullName} has not been completed. + +

Do you want to submit results anyway?

+ + + submitForm(true)} + > + Submit anyway. + + + No, go back. + + + +
- + { + e.preventDefault(); + submitForm(); + }} + > {isCorrection && correctionWarningAlert} {showDateMonthsAgoWarning && dateMonthsAgoWarningAlert} {showErrorSummary && errorSummaryAlert} @@ -474,7 +517,7 @@ const TestCardForm = ({
)} -
+
{useCurrentTime && (
-
+
@@ -546,13 +521,17 @@ const TestCardForm = ({ } className="card-dropdown" data-testid="device-type-dropdown" - errorMessage={deviceTypeIsInvalid ? "Invalid device type" : ""} + errorMessage={deviceTypeError} validationStatus={deviceTypeIsInvalid ? "error" : undefined} required={true} />
-
+
diff --git a/frontend/src/app/testQueue/TestCardForm/TestCardForm.utils.tsx b/frontend/src/app/testQueue/TestCardForm/TestCardForm.utils.tsx index ed613f4f7c..c0ee0f66aa 100644 --- a/frontend/src/app/testQueue/TestCardForm/TestCardForm.utils.tsx +++ b/frontend/src/app/testQueue/TestCardForm/TestCardForm.utils.tsx @@ -5,6 +5,12 @@ import { displayFullName } from "../../utils"; import { MULTIPLEX_DISEASES } from "../../testResults/constants"; import { MultiplexResultInput } from "../../../generated/graphql"; import { getAppInsights } from "../../TelemetryService"; +import { + ALERT_CONTENT, + QUEUE_NOTIFICATION_TYPES, + SomeoneWithName, +} from "../constants"; +import { showError, showSuccess } from "../../utils/srToast"; import { TestFormState } from "./TestCardFormReducer"; @@ -146,3 +152,23 @@ export const areAOEAnswersComplete = ( return isPregnancyAnswered && isHasAnySymptomsAnswered; } }; + +export const showTestResultDeliveryStatusAlert = ( + deliverySuccess: boolean | null | undefined, + patient: SomeoneWithName +) => { + if (deliverySuccess === false) { + const { title, body } = { + ...ALERT_CONTENT[ + QUEUE_NOTIFICATION_TYPES.DELIVERED_RESULT_TO_PATIENT__FAILURE + ](patient), + }; + return showError(body, title); + } + const { title, body } = { + ...ALERT_CONTENT[QUEUE_NOTIFICATION_TYPES.SUBMITTED_RESULT__SUCCESS]( + patient + ), + }; + showSuccess(body, title); +}; diff --git a/frontend/src/app/testQueue/constants.ts b/frontend/src/app/testQueue/constants.ts index d3815de7a2..b6ffcc8f47 100644 --- a/frontend/src/app/testQueue/constants.ts +++ b/frontend/src/app/testQueue/constants.ts @@ -4,6 +4,7 @@ import { displayFullName } from "../utils"; export const QUEUE_NOTIFICATION_TYPES = { ADDED_TO_QUEUE__SUCCESS: 1, SUBMITTED_RESULT__SUCCESS: 2, + DELIVERED_RESULT_TO_PATIENT__FAILURE: 3, }; export type SomeoneWithName = { @@ -30,19 +31,40 @@ export const ALERT_CONTENT = { body: "Newly added patients go to the bottom of the queue", }; }, - [QUEUE_NOTIFICATION_TYPES.SUBMITTED_RESULT__SUCCESS]: ( - patient: any + [QUEUE_NOTIFICATION_TYPES.SUBMITTED_RESULT__SUCCESS]: < + T extends SomeoneWithName + >( + patient: T, + startWithLastName: boolean = true ): AlertContent => { return { type: "success", title: `Result for ${displayFullName( patient.firstName, patient.middleName, - patient.lastName + patient.lastName, + startWithLastName )} was saved and reported.`, body: "See Results to view all test submissions", }; }, + [QUEUE_NOTIFICATION_TYPES.DELIVERED_RESULT_TO_PATIENT__FAILURE]: < + T extends SomeoneWithName + >( + patient: T, + startWithLastName: boolean = true + ): AlertContent => { + return { + type: "error", + title: `Unable to text result to ${displayFullName( + patient.firstName, + patient.middleName, + patient.lastName, + startWithLastName + )}`, + body: "The phone number provided may not be valid or may not be able to accept text messages", + }; + }, }; export const MIN_SEARCH_CHARACTER_COUNT = 2; From 5d5d347faa086e53b3c26741911d153b04e395a6 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 28 Sep 2023 14:10:56 -0400 Subject: [PATCH 46/78] Revert "Use storysource addon" This reverts commit 68834904bf4e2ac98442af15ec480dc4345b6074. --- frontend/.storybook/main.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 14d1109b44..6db64329db 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -6,15 +6,6 @@ module.exports = { "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app", - { - name: "@storybook/addon-storysource", - options: { - rule: { - test: [/\.stories\.[j|t]sx?$/], - include: [resolve(__dirname, "../src")], - }, - }, - }, ], webpackFinal: async (config) => { config.resolve.alias["@microsoft/applicationinsights-react-js"] = From 60f2d6d08408a1e95addcdfc17dd8e635dac5aa0 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 28 Sep 2023 15:42:24 -0400 Subject: [PATCH 47/78] Fix eslint import order --- frontend/src/app/testQueue/QueueItemSubmitLoader.tsx | 2 +- frontend/src/app/testQueue/TestCard/TestCard.tsx | 2 +- frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/testQueue/QueueItemSubmitLoader.tsx b/frontend/src/app/testQueue/QueueItemSubmitLoader.tsx index 8bb29a2abb..47ff618798 100644 --- a/frontend/src/app/testQueue/QueueItemSubmitLoader.tsx +++ b/frontend/src/app/testQueue/QueueItemSubmitLoader.tsx @@ -1,10 +1,10 @@ import classNames from "classnames"; +import { useFeature } from "flagged"; import { CSSTransition } from "react-transition-group"; import iconLoader from "../../img/loader.svg"; import "./QueueItemSubmitLoader.scss"; -import { useFeature } from "flagged"; type Props = { name: string; diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 3faeddb10d..5b1d073598 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -19,7 +19,6 @@ import Button from "../../commonComponents/Button/Button"; import { removeTimer, TestTimerWidget, useTestTimer } from "../TestTimer"; import { RootState } from "../../store"; import "./TestCard.scss"; - import TestCardForm from "../TestCardForm/TestCardForm"; import { useTestOrderPatient } from "../TestCardForm/TestCardForm.utils"; @@ -61,6 +60,7 @@ export const TestCard = ({ testCardElement.current.scrollIntoView({ behavior: "smooth" }); } // only run on first render to prevent disruptive repeated scrolls + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const timerContext = { diff --git a/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx b/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx index 8cef7ffdd6..16e00b0b2c 100644 --- a/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx @@ -28,7 +28,6 @@ import { import { removeTimer, updateTimer } from "../TestTimer"; import { showError } from "../../utils/srToast"; import "./TestCardForm.scss"; - import { TestCorrectionReason, TestCorrectionReasons, From aef712b66682ffdb7d452057696a22e7cbefeba6 Mon Sep 17 00:00:00 2001 From: Zedd Shmais Date: Mon, 2 Oct 2023 14:01:26 -0500 Subject: [PATCH 48/78] fix snap --- .../config/FeatureFlagsConfig.java | 2 - .../resources/application-azure-prod.yaml | 1 - .../__snapshots__/TestCard.test.tsx.snap | 623 ++++++++++++++++++ .../__snapshots__/QueueItem.test.tsx.snap | 13 +- 4 files changed, 630 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/config/FeatureFlagsConfig.java b/backend/src/main/java/gov/cdc/usds/simplereport/config/FeatureFlagsConfig.java index fb4c81a429..1685cc3403 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/config/FeatureFlagsConfig.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/config/FeatureFlagsConfig.java @@ -26,7 +26,6 @@ public class FeatureFlagsConfig { private boolean hivEnabled; private boolean rsvEnabled; - private boolean testCardRefactorEnabled; private boolean singleEntryRsvEnabled; private boolean agnosticEnabled; private boolean agnosticBulkUploadEnabled; @@ -42,7 +41,6 @@ private void flagMapping(String flagName, Boolean flagValue) { switch (flagName) { case "hivEnabled" -> setHivEnabled(flagValue); case "rsvEnabled" -> setRsvEnabled(flagValue); - case "testCardRefactorEnabled" -> setTestCardRefactorEnabled(flagValue); case "singleEntryRsvEnabled" -> setSingleEntryRsvEnabled(flagValue); case "agnosticEnabled" -> setAgnosticEnabled(flagValue); case "agnosticBulkUploadEnabled" -> setAgnosticBulkUploadEnabled(flagValue); diff --git a/backend/src/main/resources/application-azure-prod.yaml b/backend/src/main/resources/application-azure-prod.yaml index 51a8872952..eff9d729b1 100644 --- a/backend/src/main/resources/application-azure-prod.yaml +++ b/backend/src/main/resources/application-azure-prod.yaml @@ -25,7 +25,6 @@ twilio: features: hivEnabled: false rsvEnabled: false - testCardRefactorEnabled: false singleEntryRsvEnabled: false agnosticEnabled: false testCardRefactorEnabled: false diff --git a/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap b/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap new file mode 100644 index 0000000000..7ace5d2f2a --- /dev/null +++ b/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap @@ -0,0 +1,623 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TestCard matches snapshot 1`] = ` +
+
+
  • +
    +
    +
    +
    + +
    +
    + +
    +
    + + DOB: + 09/20/2015 + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + COVID-19 result + + + * + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + Is the patient pregnant? + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + Is the patient currently experiencing any symptoms? + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
  • +
    +
    +
    +`; diff --git a/frontend/src/app/testQueue/__snapshots__/QueueItem.test.tsx.snap b/frontend/src/app/testQueue/__snapshots__/QueueItem.test.tsx.snap index b6b45639ac..504bc83fe6 100644 --- a/frontend/src/app/testQueue/__snapshots__/QueueItem.test.tsx.snap +++ b/frontend/src/app/testQueue/__snapshots__/QueueItem.test.tsx.snap @@ -106,12 +106,6 @@ exports[`QueueItem matches snapshot 1`] = ` class="timer-button timer-reset" data-testid="timer" > - - 15:00 - - + + Start timer + +
    Date: Thu, 5 Oct 2023 12:57:27 -0500 Subject: [PATCH 49/78] fixed frontend test and fixed crash when sysmptomOnSet date was set --- .../src/app/commonComponents/TextInput.tsx | 3 +- .../app/testQueue/TestCard/TestCard.test.tsx | 1539 +++++++++-------- .../src/app/testQueue/TestCard/TestCard.tsx | 5 +- .../__snapshots__/TestCard.test.tsx.snap | 1030 +++++------ .../testQueue/TestCardForm/TestCardForm.tsx | 57 +- .../TestCardForm/TestCardForm.utils.tsx | 2 +- .../TestCardForm/TestCardFormReducer.tsx | 2 +- .../CovidAoEForm.tsx | 8 +- .../MultiplexResultInputGroup.tsx | 5 +- 9 files changed, 1359 insertions(+), 1292 deletions(-) diff --git a/frontend/src/app/commonComponents/TextInput.tsx b/frontend/src/app/commonComponents/TextInput.tsx index e6111188ba..6b18448049 100644 --- a/frontend/src/app/commonComponents/TextInput.tsx +++ b/frontend/src/app/commonComponents/TextInput.tsx @@ -96,6 +96,7 @@ export const TextInput = ({ labelClassName )} htmlFor={id} + id={`label-for-${id}`} aria-describedby={ariaDescribedBy} > {required ? : } @@ -112,7 +113,6 @@ export const TextInput = ({ "usa-input", validationStatus === "error" && "usa-input--error" )} - id={id} data-testid={idString} name={name} value={value || ""} @@ -135,6 +135,7 @@ export const TextInput = ({ ? { "aria-describedby": `error_${id}`, "aria-invalid": true } : null)} {...registrationProps} + id={id} />
    )} diff --git a/frontend/src/app/testQueue/TestCard/TestCard.test.tsx b/frontend/src/app/testQueue/TestCard/TestCard.test.tsx index bb2c783fb7..923c836e21 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.test.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.test.tsx @@ -10,21 +10,22 @@ import { } from "@testing-library/react"; import moment from "moment"; import userEvent from "@testing-library/user-event"; -import { MemoryRouter } from "react-router-dom"; import { getAppInsights } from "../../TelemetryService"; import * as srToast from "../../utils/srToast"; import { SubmitQueueItemDocument as SUBMIT_TEST_RESULT, EditQueueItemDocument as EDIT_QUEUE_ITEM, + UpdateAoeDocument as UPDATE_AOE, PhoneType, SubmitQueueItemMutationVariables as SUBMIT_QUEUE_ITEM_VARIABLES, EditQueueItemMutationVariables as EDIT_QUEUE_ITEM_VARIABLES, + UpdateAoeMutationVariables as UPDATE_AOE_VARIABLES, SubmitQueueItemMutation as SUBMIT_QUEUE_ITEM_DATA, EditQueueItemMutation as EDIT_QUEUE_ITEM_DATA, + UpdateAoeMutation as UPDATE_AOE_DATA, } from "../../../generated/graphql"; import SRToastContainer from "../../commonComponents/SRToastContainer"; -import PrimeErrorBoundary from "../../PrimeErrorBoundary"; import { TestCorrectionReason } from "../../testResults/TestResultCorrectionModal"; import { QueriedFacility, QueriedTestOrder } from "../QueueItem"; import mockSupportedDiseaseCovid from "../mocks/mockSupportedDiseaseCovid"; @@ -58,21 +59,21 @@ const device3Name = "BD Veritor"; const device4Name = "Multiplex"; const device5Name = "MultiplexAndCovidOnly"; -const device1Id = "ee4f40b7-ac32-4709-be0a-56dd77bb9609"; -const device2Id = "5c711888-ba37-4b2e-b347-311ca364efdb"; -const device3Id = "32b2ca2a-75e6-4ebd-a8af-b50c7aea1d10"; -const device4Id = "67109f6f-eaee-49d3-b8ff-c61b79a9da8e"; -const device5Id = "da524a8e-672d-4ff4-a4ec-c1e14d0337db"; +const device1Id = "DEVICE-1-ID"; +const device2Id = "DEVICE-2-ID"; +const device3Id = "DEVICE-3-ID"; +const device4Id = "DEVICE-4-ID"; +const device5Id = "DEVICE-5-ID"; -const deletedDeviceId = "8ab0cafa-8e36-48d6-91fc-6352405e1d91"; +const deletedDeviceId = "DELETED-DEVICE-ID"; const deletedDeviceName = "Deleted"; const specimen1Name = "Swab of internal nose"; -const specimen1Id = "8596682d-6053-4720-8a39-1f5d19ff4ed9"; +const specimen1Id = "SPECIMEN-1-ID"; const specimen2Name = "Nasopharyngeal swab"; -const specimen2Id = "f127ef55-4133-4556-9bca-33615d071e8d"; +const specimen2Id = "SPECIMEN-2-ID"; -const deletedSpecimenId = "fddb9ef4-7606-4621-b7a2-20772bac5136"; +const deletedSpecimenId = "DELETED-SPECIMEN-ID"; const getDeviceTypeDropdown = async () => (await screen.findByTestId("device-type-dropdown")) as HTMLSelectElement; @@ -86,12 +87,15 @@ async function getSpecimenTypeDropdown() { describe("TestCard", () => { let nowFn = Date.now; let store: MockStoreEnhanced; + let alertSpy: jest.SpyInstance; const mockStore = configureStore([]); const trackEventMock = jest.fn(); + const removePatientFromQueueMock = jest.fn(); + const trackMetricMock = jest.fn(); + const trackExceptionMock = jest.fn(); const testOrderInfo: QueriedTestOrder = { internalId: "1b02363b-ce71-4f30-a2d6-d82b56a91b39", - pregnancy: "60001007", dateAdded: "2022-11-08 13:33:07.503", symptoms: '{"64531003":"false","103001002":"false","84229001":"false","68235000":"false","426000000":"false","49727002":"false","68962001":"false","422587007":"false","267036007":"false","62315008":"false","43724002":"false","36955009":"false","44169009":"false","422400008":"false","230145002":"false","25064002":"false","162397003":"false"}', @@ -251,7 +255,7 @@ describe("TestCard", () => { devicesMap: devicesMap, startTestPatientId: "", setStartTestPatientId: setStartTestPatientIdMock, - removePatientFromQueue: jest.fn().mockReturnValue(null), + removePatientFromQueue: removePatientFromQueueMock, }; type testRenderProps = { @@ -259,34 +263,38 @@ describe("TestCard", () => { mocks?: any; }; - const renderQueueItem = async ( - { props, mocks }: testRenderProps = { props: testProps, mocks: undefined } - ) => { + async function renderQueueItem( + { props, mocks }: testRenderProps = { props: testProps, mocks: [] } + ) { props = props || testProps; - const { container } = render( - + <> - - - - - + + + - + ); - await new Promise((resolve) => setTimeout(resolve, 501)); - return container; - }; + return { container, user: userEvent.setup() }; + } beforeEach(() => { store = mockStore({ @@ -297,9 +305,12 @@ describe("TestCard", () => { (getAppInsights as jest.Mock).mockImplementation(() => ({ trackEvent: trackEventMock, + trackMetric: trackMetricMock, + trackException: trackExceptionMock, })); - jest.spyOn(console, "error").mockImplementation(() => {}); + // jest.spyOn(console, "error").mockImplementation(() => {}); jest.spyOn(global.Math, "random").mockReturnValue(1); + alertSpy = jest.spyOn(srToast, "showError"); }); afterEach(() => { @@ -307,6 +318,7 @@ describe("TestCard", () => { (getAppInsights as jest.Mock).mockReset(); jest.spyOn(console, "error").mockRestore(); jest.spyOn(global.Math, "random").mockRestore(); + alertSpy.mockRestore(); }); afterAll(() => { @@ -413,489 +425,17 @@ describe("TestCard", () => { ).toBeTruthy(); }); - it("correctly handles when device is deleted from facility", async () => { - const mocks = [ - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: deletedDeviceId, - specimenTypeId: specimen1Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: deletedDeviceId, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device1Id, - specimenTypeId: specimen1Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device1Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - ]; - - const props = { - ...testProps, - testOrder: { - ...testProps.testOrder, - deviceType: { - internalId: deletedDeviceId, - name: deletedDeviceName, - model: "test", - testLength: 12, - supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, - }, - correctionStatus: "CORRECTED", - reasonForCorrection: TestCorrectionReason.INCORRECT_RESULT, - }, - }; - - await renderQueueItem({ props, mocks }); - - const deviceDropdown = await getDeviceTypeDropdown(); - expect(deviceDropdown.options.length).toEqual(6); - expect(deviceDropdown.options[0].label).toEqual(""); - expect(deviceDropdown.options[1].label).toEqual("Abbott BinaxNow"); - expect(deviceDropdown.options[2].label).toEqual("BD Veritor"); - expect(deviceDropdown.options[3].label).toEqual("LumiraDX"); - expect(deviceDropdown.options[4].label).toEqual("Multiplex"); - expect(deviceDropdown.options[5].label).toEqual("MultiplexAndCovidOnly"); - - expect(deviceDropdown.value).toEqual(""); - - const swabDropdown = await getSpecimenTypeDropdown(); - expect(swabDropdown.options.length).toEqual(0); - expect(swabDropdown).toBeDisabled(); - - // notice the initial error message - expect(screen.getByText("Please select a device.")).toBeInTheDocument(); - - const submitButton = screen.getByText("Submit results") as HTMLInputElement; - await userEvent.click(submitButton); - - // attempting to submit should show error toast - expect(screen.getByText("Invalid test device")).toBeInTheDocument(); - - await userEvent.selectOptions(deviceDropdown, device1Id); - - // error goes away after selecting a valid device - const deviceTypeDropdownContainer = screen.getByTestId( - "device-type-dropdown-container" - ); - expect( - within(deviceTypeDropdownContainer).queryByText("Please select a device.") - ).not.toBeInTheDocument(); - - await userEvent.click(submitButton); - - // able to submit after selecting valid device - // submit modal appears when able to submit but AOE responses are incomplete - expect(screen.getByText("Submit anyway.")).toBeInTheDocument(); - }); - - it("correctly handles when swab is deleted from device", async () => { - const mocks = [ - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device2Id, - specimenTypeId: deletedSpecimenId, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device2Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device1Id, - specimenTypeId: specimen1Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device1Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - ]; - - const props = { - ...testProps, - testOrder: { - ...testProps.testOrder, - deviceType: { - internalId: device2Id, - name: device2Name, - model: "test", - testLength: 12, - supportedDiseases: [ - { - internalId: "6e67ea1c-f9e8-4b3f-8183-b65383ac1283", - loinc: "96741-4", - name: "COVID-19", - }, - ], - }, - specimenType: { - name: "deleted", - internalId: deletedSpecimenId, - typeCode: "12345", - }, - correctionStatus: "CORRECTED", - reasonForCorrection: TestCorrectionReason.INCORRECT_RESULT, - }, - }; - - await renderQueueItem({ props, mocks }); - - const deviceDropdown = await getDeviceTypeDropdown(); - expect(deviceDropdown.options.length).toEqual(5); - expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); - expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); - expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); - expect(deviceDropdown.options[3].label).toEqual("Multiplex"); - expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); - expect(deviceDropdown.value).toEqual(device2Id); - - const swabDropdown = await getSpecimenTypeDropdown(); - expect(swabDropdown.options.length).toEqual(2); - expect(swabDropdown.options[0].label).toEqual(""); - expect(swabDropdown.options[1].label).toEqual("Swab of internal nose"); - - // disables submitting results and changing date - const currentDateTimeButton = screen.getByLabelText("Current date/time", { - exact: false, - }) as HTMLInputElement; - const positiveResult = screen.getByLabelText("Positive", { - exact: false, - }) as HTMLInputElement; - const submitButton = screen.getByText("Submit") as HTMLInputElement; - - expect(currentDateTimeButton).toBeDisabled(); - expect(positiveResult).toBeDisabled(); - expect(submitButton).toBeDisabled(); - - // notice the error message - expect(screen.getByText("Please select a swab type")).toBeInTheDocument(); - - await userEvent.selectOptions(swabDropdown, specimen1Id); - await new Promise((resolve) => setTimeout(resolve, 501)); - - // error goes away after selecting a valid device - expect( - screen.queryByText("Please select a swab type") - ).not.toBeInTheDocument(); - - // enable selecting date/time and results - expect(positiveResult).toBeEnabled(); - expect(currentDateTimeButton).toBeEnabled(); - - // enables submitting of results after selecting one - await userEvent.click(positiveResult); - await new Promise((resolve) => setTimeout(resolve, 501)); - expect(submitButton).toBeEnabled(); - }); - - describe("on device specimen type change", () => { - const mocks = [ - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device1Id, - specimenTypeId: specimen1Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device1Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device3Id, - specimenTypeId: specimen1Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device3Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device3Id, - specimenTypeId: specimen2Id, - results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device3Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device4Id, - specimenTypeId: specimen1Id, - results: [], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "POSITIVE", - }, - ], - dateTested: null, - deviceType: { - internalId: device4Id, - testLength: 10, - supportedDiseases: [ - { - internalId: "6e67ea1c-f9e8-4b3f-8183-b65383ac1283", - loinc: "96741-4", - name: "COVID-19", - }, - { - internalId: "e286f2a8-38e2-445b-80a5-c16507a96b66", - loinc: "LP14239-5", - name: "Flu A", - }, - { - internalId: "14924488-268f-47db-bea6-aa706971a098", - loinc: "LP14240-3", - name: "Flu B", - }, - ], - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - ]; - - it("updates test order on device type and specimen type change", async () => { - await renderQueueItem({ mocks }); - - const deviceDropdown = await getDeviceTypeDropdown(); - expect(deviceDropdown.options.length).toEqual(5); - expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); - expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); - expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); - expect(deviceDropdown.options[3].label).toEqual("Multiplex"); - expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); - - // select results - await userEvent.click( - screen.getByLabelText("Positive", { exact: false }) - ); - await new Promise((resolve) => setTimeout(resolve, 501)); - - // Change device type - await userEvent.selectOptions(deviceDropdown, device3Name); - await new Promise((resolve) => setTimeout(resolve, 501)); - - // Change specimen type - const swabDropdown = await getSpecimenTypeDropdown(); - expect(swabDropdown.options.length).toEqual(2); - expect(swabDropdown.options[0].label).toEqual("Nasopharyngeal swab"); - expect(swabDropdown.options[1].label).toEqual("Swab of internal nose"); - - await userEvent.selectOptions(swabDropdown, specimen2Name); - await new Promise((resolve) => setTimeout(resolve, 501)); - - expect(deviceDropdown.value).toEqual(device3Id); - expect(swabDropdown.value).toEqual(specimen2Id); - - await waitFor( - () => { - expect(screen.getByText("Submit")).toBeEnabled(); - }, - { timeout: 1000 } - ); - }); - - it("adds radio buttons for Flu A and Flu B when a multiplex device is chosen", async () => { - await renderQueueItem({ mocks }); - - expect(screen.queryByText("Flu A")).not.toBeInTheDocument(); - expect(screen.queryByText("Flu B")).not.toBeInTheDocument(); - - const deviceDropdown = await getDeviceTypeDropdown(); - expect(deviceDropdown.options.length).toEqual(5); - expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); - expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); - expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); - expect(deviceDropdown.options[3].label).toEqual("Multiplex"); - expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); - - // Change device type to a multiplex device - await userEvent.selectOptions(deviceDropdown, device4Name); - await new Promise((resolve) => setTimeout(resolve, 501)); - - expect(screen.getByText("Flu A")).toBeInTheDocument(); - expect(screen.getByText("Flu B")).toBeInTheDocument(); - }); - }); - - describe("SMS delivery failure", () => { - let alertSpy: jest.SpyInstance; - beforeEach(() => { - alertSpy = jest.spyOn(srToast, "showError"); - }); - - afterEach(() => { - alertSpy.mockRestore(); - }); - - it("displays delivery failure alert on submit for invalid patient phone number", async () => { + describe("when a selected device or specimen is deleted", () => { + it("correctly handles when device is deleted from facility", async () => { const mocks = [ { request: { query: EDIT_QUEUE_ITEM, variables: { id: testOrderInfo.internalId, - deviceTypeId: device1Id, + deviceTypeId: deletedDeviceId, specimenTypeId: specimen1Id, - results: [ - { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, - ], + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], dateTested: null, } as EDIT_QUEUE_ITEM_VARIABLES, }, @@ -905,12 +445,12 @@ describe("TestCard", () => { results: [ { disease: { name: "COVID-19" }, - testResult: "UNDETERMINED", + testResult: "POSITIVE", }, ], dateTested: null, deviceType: { - internalId: device1Id, + internalId: deletedDeviceId, testLength: 10, }, }, @@ -919,26 +459,31 @@ describe("TestCard", () => { }, { request: { - query: SUBMIT_TEST_RESULT, + query: EDIT_QUEUE_ITEM, variables: { - patientId: testOrderInfo.patient.internalId, + id: testOrderInfo.internalId, deviceTypeId: device1Id, specimenTypeId: specimen1Id, - results: [ - { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, - ], + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], dateTested: null, - } as SUBMIT_QUEUE_ITEM_VARIABLES, + } as EDIT_QUEUE_ITEM_VARIABLES, }, result: { data: { - submitQueueItem: { - testResult: { - internalId: testOrderInfo.internalId, + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device1Id, + testLength: 10, }, - deliverySuccess: false, }, - } as SUBMIT_QUEUE_ITEM_DATA, + } as EDIT_QUEUE_ITEM_DATA, }, }, ]; @@ -947,45 +492,293 @@ describe("TestCard", () => { ...testProps, testOrder: { ...testProps.testOrder, - pregnancy: null, - symptomOnset: null, - noSymptoms: null, + deviceType: { + internalId: deletedDeviceId, + name: deletedDeviceName, + model: "test", + testLength: 12, + supportedDiseaseTestPerformed: mockSupportedDiseaseCovid, + }, + correctionStatus: "CORRECTED", + reasonForCorrection: TestCorrectionReason.INCORRECT_RESULT, }, }; await renderQueueItem({ props, mocks }); - // Select result - await userEvent.click( - screen.getByLabelText("Inconclusive", { - exact: false, - }) - ); + const deviceDropdown = await getDeviceTypeDropdown(); + expect(deviceDropdown.options.length).toEqual(6); + expect(deviceDropdown.options[0].label).toEqual(""); + expect(deviceDropdown.options[1].label).toEqual("Abbott BinaxNow"); + expect(deviceDropdown.options[2].label).toEqual("BD Veritor"); + expect(deviceDropdown.options[3].label).toEqual("LumiraDX"); + expect(deviceDropdown.options[4].label).toEqual("Multiplex"); + expect(deviceDropdown.options[5].label).toEqual("MultiplexAndCovidOnly"); - // Wait for the genuinely long-running "edit queue" operation to finish - await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(deviceDropdown.value).toEqual(""); - // Submit - await userEvent.click(screen.getByText("Submit")); - await new Promise((resolve) => setTimeout(resolve, 1000)); + const swabDropdown = await getSpecimenTypeDropdown(); + expect(swabDropdown.options.length).toEqual(0); + expect(swabDropdown).toBeDisabled(); - await userEvent.click( - screen.getByText("Submit anyway", { - exact: false, - }) - ); + // notice the initial error message + expect(screen.getByText("Please select a device.")).toBeInTheDocument(); - // Displays submitting indicator + const submitButton = screen.getByText( + "Submit results" + ) as HTMLInputElement; + await userEvent.click(submitButton); + + // attempting to submit should show error toast + expect(screen.getByText("Invalid test device")).toBeInTheDocument(); + + await userEvent.selectOptions(deviceDropdown, device1Id); + + // error goes away after selecting a valid device + const deviceTypeDropdownContainer = screen.getByTestId( + "device-type-dropdown-container" + ); expect( - await screen.findByText( - "Submitting test data for Dixon, Althea Hedda Mclaughlin..." + within(deviceTypeDropdownContainer).queryByText( + "Please select a device." ) - ).toBeInTheDocument(); + ).not.toBeInTheDocument(); - // Wait for MockedProvider to populate the mocked result - await new Promise((resolve) => setTimeout(resolve, 0)); + await userEvent.click(submitButton); - // Verify alert is displayed + // able to submit after selecting valid device + // submit modal appears when able to submit but AOE responses are incomplete + expect(screen.getByText("Submit anyway.")).toBeInTheDocument(); + }); + + it("correctly handles when specimen is deleted from device", async () => { + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device2Id, + specimenTypeId: deletedSpecimenId, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device2Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device2Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device1Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + const props = { + ...testProps, + testOrder: { + ...testProps.testOrder, + deviceType: { + internalId: device2Id, + name: device2Name, + model: "test", + testLength: 12, + supportedDiseases: [ + { + internalId: "6e67ea1c-f9e8-4b3f-8183-b65383ac1283", + loinc: "96741-4", + name: "COVID-19", + }, + ], + }, + specimenType: { + name: "deleted", + internalId: deletedSpecimenId, + typeCode: "12345", + }, + correctionStatus: "CORRECTED", + reasonForCorrection: TestCorrectionReason.INCORRECT_RESULT, + }, + }; + + await renderQueueItem({ props, mocks }); + + const deviceDropdown = await getDeviceTypeDropdown(); + expect(deviceDropdown.options.length).toEqual(5); + expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); + expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); + expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); + expect(deviceDropdown.options[3].label).toEqual("Multiplex"); + expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); + expect(deviceDropdown.value).toEqual(device2Id); + + const swabDropdown = await getSpecimenTypeDropdown(); + expect(swabDropdown.options.length).toEqual(2); + expect(swabDropdown.options[0].label).toEqual(""); + expect(swabDropdown.options[1].label).toEqual("Swab of internal nose"); + + // disables submitting results and changing date + // const currentDateTimeButton = screen.getByLabelText("Current date and time", { + // exact: false, + // }) as HTMLInputElement; + // const positiveResult = screen.getByLabelText("Positive", { + // exact: false, + // }) as HTMLInputElement; + // const submitButton = screen.getByText("Submit results") as HTMLInputElement; + + // expect(currentDateTimeButton).toBeDisabled(); + // expect(positiveResult).toBeDisabled(); + // expect(submitButton).toBeDisabled(); + + // notice the error message + expect( + screen.getByText("Please select a specimen type.") + ).toBeInTheDocument(); + + await userEvent.selectOptions(swabDropdown, specimen1Id); + + // error goes away after selecting a valid device + expect( + screen.queryByText("Please select a specimen type.") + ).not.toBeInTheDocument(); + }); + }); + + describe("SMS delivery failure", () => { + it("displays delivery failure alert on submit for invalid patient phone number", async () => { + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device1Id, + specimenTypeId: specimen1Id, + results: [ + { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, + ], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "UNDETERMINED", + }, + ], + dateTested: null, + deviceType: { + internalId: device1Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: SUBMIT_TEST_RESULT, + variables: { + patientId: testOrderInfo.patient.internalId, + deviceTypeId: device1Id, + specimenTypeId: specimen1Id, + results: [ + { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, + ], + dateTested: null, + } as SUBMIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + submitQueueItem: { + testResult: { + internalId: testOrderInfo.internalId, + }, + deliverySuccess: false, + }, + } as SUBMIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + const props = { + ...testProps, + testOrder: { + ...testProps.testOrder, + pregnancy: null, + symptomOnset: null, + noSymptoms: null, + }, + }; + + await renderQueueItem({ props, mocks }); + + // Select result + await userEvent.click( + screen.getByLabelText("Inconclusive", { + exact: false, + }) + ); + + // Submit + await userEvent.click(screen.getByText("Submit results")); + + await userEvent.click( + screen.getByText("Submit anyway", { + exact: false, + }) + ); + + // Displays submitting indicator + expect( + await screen.findByText( + "Submitting test data for Dixon, Althea Hedda Mclaughlin..." + ) + ).toBeInTheDocument(); + + // Verify alert is displayed await waitFor(() => { expect(alertSpy).toHaveBeenCalledWith( "The phone number provided may not be valid or may not be able to accept text messages", @@ -1015,7 +808,7 @@ describe("TestCard", () => { it("updates custom test date/time", async () => { await renderQueueItem(); - const toggle = await screen.findByLabelText("Current date/time"); + const toggle = await screen.findByLabelText("Current date and time"); await userEvent.click(toggle); const dateInput = screen.getByTestId("test-date"); expect(dateInput).toBeInTheDocument(); @@ -1025,172 +818,45 @@ describe("TestCard", () => { userEvent.type(timeInput, updatedTimeString); }); - it("does not allow future date for test date", async () => { - const mocks = [ - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device1Id, - specimenTypeId: specimen1Id, - results: [], - dateTested: null, - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "UNDETERMINED", - }, - ], - dateTested: null, - deviceType: { - internalId: device1Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: EDIT_QUEUE_ITEM, - variables: { - id: testOrderInfo.internalId, - deviceTypeId: device1Id, - specimenTypeId: specimen1Id, - results: [], - dateTested: "2022-07-15T12:35:00.000Z", - } as EDIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - editQueueItem: { - results: [ - { - disease: { name: "COVID-19" }, - testResult: "UNDETERMINED", - }, - ], - dateTested: null, - deviceType: { - internalId: device1Id, - testLength: 10, - }, - }, - } as EDIT_QUEUE_ITEM_DATA, - }, - }, - { - request: { - query: SUBMIT_TEST_RESULT, - variables: { - patientId: testOrderInfo.patient.internalId, - deviceTypeId: testOrderInfo.deviceType.internalId, - specimenTypeId: testOrderInfo.specimenType.internalId, - results: [{ diseaseName: "COVID-19", testResult: "UNDETERMINED" }], - dateTested: "2022-07-15T12:35:00.000Z", - } as SUBMIT_QUEUE_ITEM_VARIABLES, - }, - result: { - data: { - submitQueueItem: { - testResult: { - internalId: testOrderInfo.internalId, - }, - deliverySuccess: true, - }, - } as SUBMIT_QUEUE_ITEM_DATA, - }, + it("shows error for future test date", async () => { + await renderQueueItem({ + props: { + ...testProps, + testOrder: { ...testOrderInfo, dateTested: "2100-07-15T12:35:00.000Z" }, }, - ]; - - await renderQueueItem({ mocks }); - - const toggle = await screen.findByLabelText("Current date/time"); - await userEvent.click(toggle); - await new Promise((resolve) => setTimeout(resolve, 501)); - - const dateInput = screen.getByTestId("test-date"); - expect(dateInput).toBeInTheDocument(); - const timeInput = screen.getByTestId("test-time"); - expect(timeInput).toBeInTheDocument(); - - // Select result - await userEvent.click( - screen.getByLabelText("Inconclusive", { exact: true }) - ); - - // There is a 500ms debounce on queue item update operations - await new Promise((resolve) => setTimeout(resolve, 501)); - - // Input invalid (future date) - can't submit - const futureDateTime = moment({ - year: 2050, - month: 6, - day: 15, - hour: 12, - minute: 35, }); - await userEvent.type(dateInput, futureDateTime.format("YYYY-MM-DD")); - await userEvent.type(timeInput, futureDateTime.format("hh:mm")); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - await waitFor( - () => { - expect(screen.getByText("Submit")).toBeEnabled(); - }, - { timeout: 1000 } - ); - - // Submit test - await userEvent.click(await screen.findByText("Submit")); - // 500ms debounce on queue item update operations plus a little extra wait time - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Toast alert should appear - expect(await screen.findByText("Invalid test date")).toBeInTheDocument(); - const updatedTestCard = await screen.findByTestId( - `test-card-${testOrderInfo.patient.internalId}` - ); - expect(updatedTestCard).toHaveClass("prime-queue-item__error"); - const dateLabel = await screen.findByText("Test date and time"); - expect(dateLabel).toHaveClass("queue-item-error-message"); - const updatedDateInput = await screen.findByTestId("test-date"); - expect(updatedDateInput).toHaveClass("card-test-input__error"); + expect( + screen.getByText("Test date can't be in the future.", { + exact: false, + }) + ).toBeInTheDocument(); }); it("formats card with warning state if selected date input is more than six months ago", async () => { await renderQueueItem(); - const toggle = await screen.findByLabelText("Current date/time"); + const toggle = await screen.findByLabelText("Current date and time"); await userEvent.click(toggle); const dateInput = screen.getByTestId("test-date"); - const timeInput = screen.getByTestId("test-time"); - const oldDate = moment({ year: 2022, month: 1, day: 1 }); fireEvent.change(dateInput, { target: { value: oldDate.format("YYYY-MM-DD") }, }); - const testCard = await screen.findByTestId( - `test-card-${testOrderInfo.patient.internalId}` - ); - expect(testCard).toHaveClass("prime-queue-item__ready"); - expect(dateInput).toHaveClass("card-correction-input"); - expect(timeInput).toHaveClass("card-correction-input"); - expect(screen.getByTestId("test-correction-header")).toBeInTheDocument(); + expect( + screen.getByText( + "The date you selected is more than six months ago. Please make sure it's correct before submitting.", + { + exact: false, + } + ) + ).toBeInTheDocument(); }); - it("highlights test corrections and includes corrector name and reason for correction", async () => { + it("warn of test corrections and reason for correction", async () => { const props = { ...testProps, testOrder: { @@ -1201,41 +867,99 @@ describe("TestCard", () => { }; await renderQueueItem({ props }); - const testCard = await screen.findByTestId( - `test-card-${testOrderInfo.patient.internalId}` - ); // Card is highlighted for visibility - expect(testCard).toHaveClass("prime-queue-item__ready"); - - expect( - await within(testCard).findByText("Incorrect test result", { - exact: false, - }) - ).toBeInTheDocument(); - }); - - it("displays person's mobile phone numbers", async () => { - await renderQueueItem(); - - const questionnaire = await screen.findByText("Test questionnaire"); - await userEvent.click(questionnaire); + const alert = within( + screen.getByTestId(`test-card-${testOrderInfo.patient.internalId}`) + ).getByTestId("alert"); + expect(alert).toHaveClass("usa-alert--warning"); - await screen.findByText("Required fields are marked", { exact: false }); expect( - screen.getByText(testProps.testOrder.patient.phoneNumbers![0]!.number!, { + await within(alert).findByText("Incorrect test result", { exact: false, }) ).toBeInTheDocument(); - expect( - screen.queryByText(testProps.testOrder.patient.phoneNumbers![1]!.number!) - ).not.toBeInTheDocument(); }); describe("when device supports covid only and multiplex", () => { it("should allow you to submit covid only results", async () => { - await renderQueueItem({}); - + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device4Id, + specimenTypeId: specimen1Id, + results: [], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [], + dateTested: null, + deviceType: { + internalId: device4Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device4Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [], + dateTested: null, + deviceType: { + internalId: device4Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device5Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [], + dateTested: null, + deviceType: { + internalId: device5Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + await renderQueueItem({ mocks }); + const deviceDropdown = await getDeviceTypeDropdown(); expect(deviceDropdown.options.length).toEqual(5); expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); @@ -1246,7 +970,6 @@ describe("TestCard", () => { // Change device type to multiplex await userEvent.selectOptions(deviceDropdown, device4Name); - await new Promise((resolve) => setTimeout(resolve, 501)); // select results await userEvent.click( @@ -1254,39 +977,93 @@ describe("TestCard", () => { screen.getByTestId(`covid-test-result-${testOrderInfo.internalId}`) ).getByLabelText("Positive", { exact: false }) ); - await new Promise((resolve) => setTimeout(resolve, 501)); - - // Notice submit is disabled - expect(screen.getByText("Submit")).toBeDisabled(); // Change device type to multiplex that supports covid only await userEvent.selectOptions(deviceDropdown, device5Name); - await new Promise((resolve) => setTimeout(resolve, 501)); expect(deviceDropdown.value).toEqual(device5Id); // Notice submit is enabled - expect(screen.getByText("Submit")).toBeEnabled(); + expect(screen.getByText("Submit results")).toBeEnabled(); }); }); - describe("telemetry", () => { - beforeEach(async () => { + describe("test submission and telemetry", () => { + it("delegates removal of patient from queue to removePatientFromQueue hook", async () => { await renderQueueItem(); - }); - it("tracks removal of patient from queue as custom event", async () => { const button = screen.getByLabelText( `Close test for Dixon, Althea Hedda Mclaughlin` ); await userEvent.click(button); const iAmSure = screen.getByText("Yes, I'm sure"); await userEvent.click(iAmSure); - expect(trackEventMock).toHaveBeenCalledWith({ - name: "Remove Patient From Queue", - }); + + expect(removePatientFromQueueMock).toHaveBeenCalledWith( + testOrderInfo.patient.internalId + ); }); it("tracks submitted test result as custom event", async () => { + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device1Id, + specimenTypeId: specimen1Id, + results: [ + { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, + ], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device1Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: SUBMIT_TEST_RESULT, + variables: { + patientId: testOrderInfo.patient.internalId, + deviceTypeId: device1Id, + specimenTypeId: specimen1Id, + results: [ + { diseaseName: "COVID-19", testResult: "UNDETERMINED" }, + ], + dateTested: null, + } as SUBMIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + submitQueueItem: { + testResult: { + internalId: testOrderInfo.internalId, + }, + deliverySuccess: false, + }, + } as SUBMIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + await renderQueueItem({ mocks }); + // Select result await userEvent.click( screen.getByLabelText("Inconclusive", { @@ -1294,12 +1071,9 @@ describe("TestCard", () => { }) ); - // Wait for the genuinely long-running "edit queue" operation to finish - await new Promise((resolve) => setTimeout(resolve, 1000)); - // Submit - await userEvent.click(screen.getByText("Submit")); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await userEvent.click(screen.getByText("Submit results")); + await userEvent.click(screen.getByText("Submit anyway.")); expect(trackEventMock).toHaveBeenCalledWith({ name: "Submit Test Result", @@ -1307,21 +1081,282 @@ describe("TestCard", () => { }); it("tracks AoE form updates as custom event", async () => { - // Update AoE questionnaire - const questionnaire = await screen.findByText("Test questionnaire"); - await userEvent.click(questionnaire); - const symptomInput = await screen.findByText("No symptoms", { - exact: false, - }); - await userEvent.click(symptomInput); + const mocks = [ + { + request: { + query: UPDATE_AOE, + variables: { + patientId: testOrderInfo.patient.internalId, + symptoms: + '{"64531003":"false","103001002":"false","84229001":"false","68235000":"false","426000000":"false","49727002":"false","68962001":"false","422587007":"false","267036007":"false","62315008":"false","43724002":"false","36955009":"false","44169009":"false","422400008":"false","230145002":"false","25064002":"false","162397003":"false"}', + symptomOnset: null, + noSymptoms: false, + pregnancy: undefined, + } as UPDATE_AOE_VARIABLES, + }, + result: { + data: { + updateTimeOfTestQuestions: null, + } as UPDATE_AOE_DATA, + }, + }, + { + request: { + query: UPDATE_AOE, + variables: { + patientId: testOrderInfo.patient.internalId, + symptoms: + '{"64531003":"false","103001002":"false","84229001":"false","68235000":"false","426000000":"false","49727002":"false","68962001":"false","422587007":"false","267036007":"false","62315008":"false","43724002":"false","36955009":"false","44169009":"false","422400008":"false","230145002":"false","25064002":"false","162397003":"false"}', + symptomOnset: "2023-08-15", + noSymptoms: false, + } as UPDATE_AOE_VARIABLES, + }, + result: { + data: { + updateTimeOfTestQuestions: null, + } as UPDATE_AOE_DATA, + }, + }, + ]; - // Save changes - const continueButton = await screen.findByText("Continue"); - await userEvent.click(continueButton); + const { user } = await renderQueueItem({ mocks }); + + await user.click( + within( + screen.getByTestId(`has-any-symptoms-${testOrderInfo.internalId}`) + ).getByLabelText("Yes") + ); + await waitFor(() => + expect( + within( + screen.getByTestId(`has-any-symptoms-${testOrderInfo.internalId}`) + ).getByLabelText("Yes") + ).toBeChecked() + ); + + await user.type( + screen.getByLabelText("When did the patient's symptoms start?"), + "2023-08-15" + ); + await waitFor(() => + expect( + screen.getByLabelText("When did the patient's symptoms start?") + ).toHaveValue("2023-08-15") + ); expect(trackEventMock).toHaveBeenCalledWith({ name: "Update AoE Response", }); }); }); + + describe("on device specimen type change", () => { + it("updates test order on device type and specimen type change", async () => { + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device1Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device1Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device3Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device3Id, + testLength: 10, + supportedDiseases: [ + { + internalId: "6e67ea1c-f9e8-4b3f-8183-b65383ac1283", + loinc: "96741-4", + name: "COVID-19", + }, + { + internalId: "e286f2a8-38e2-445b-80a5-c16507a96b66", + loinc: "LP14239-5", + name: "Flu A", + }, + { + internalId: "14924488-268f-47db-bea6-aa706971a098", + loinc: "LP14240-3", + name: "Flu B", + }, + ], + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device3Id, + specimenTypeId: specimen2Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device3Id, + testLength: 10, + supportedDiseases: [ + { + internalId: "6e67ea1c-f9e8-4b3f-8183-b65383ac1283", + loinc: "96741-4", + name: "COVID-19", + }, + { + internalId: "e286f2a8-38e2-445b-80a5-c16507a96b66", + loinc: "LP14239-5", + name: "Flu A", + }, + { + internalId: "14924488-268f-47db-bea6-aa706971a098", + loinc: "LP14240-3", + name: "Flu B", + }, + ], + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + await renderQueueItem({ mocks }); + + const deviceDropdown = await getDeviceTypeDropdown(); + expect(deviceDropdown.options.length).toEqual(5); + expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); + expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); + expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); + expect(deviceDropdown.options[3].label).toEqual("Multiplex"); + expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); + + // select results + await userEvent.click( + screen.getByLabelText("Positive", { exact: false }) + ); + + // Change device type + await userEvent.selectOptions(deviceDropdown, device3Name); + + // Change specimen type + const swabDropdown = await getSpecimenTypeDropdown(); + expect(swabDropdown.options.length).toEqual(2); + expect(swabDropdown.options[0].label).toEqual("Nasopharyngeal swab"); + expect(swabDropdown.options[1].label).toEqual("Swab of internal nose"); + + await userEvent.selectOptions(swabDropdown, specimen2Name); + + expect(deviceDropdown.value).toEqual(device3Id); + expect(swabDropdown.value).toEqual(specimen2Id); + }); + + it("adds radio buttons for Flu A and Flu B when a multiplex device is chosen", async () => { + const mocks = [ + { + request: { + query: EDIT_QUEUE_ITEM, + variables: { + id: testOrderInfo.internalId, + deviceTypeId: device4Id, + specimenTypeId: specimen1Id, + results: [{ diseaseName: "COVID-19", testResult: "POSITIVE" }], + dateTested: null, + } as EDIT_QUEUE_ITEM_VARIABLES, + }, + result: { + data: { + editQueueItem: { + results: [ + { + disease: { name: "COVID-19" }, + testResult: "POSITIVE", + }, + ], + dateTested: null, + deviceType: { + internalId: device4Id, + testLength: 10, + }, + }, + } as EDIT_QUEUE_ITEM_DATA, + }, + }, + ]; + + await renderQueueItem({ mocks }); + + expect(screen.queryByText("Flu A result")).not.toBeInTheDocument(); + expect(screen.queryByText("Flu B result")).not.toBeInTheDocument(); + + const deviceDropdown = await getDeviceTypeDropdown(); + expect(deviceDropdown.options.length).toEqual(5); + expect(deviceDropdown.options[0].label).toEqual("Abbott BinaxNow"); + expect(deviceDropdown.options[1].label).toEqual("BD Veritor"); + expect(deviceDropdown.options[2].label).toEqual("LumiraDX"); + expect(deviceDropdown.options[3].label).toEqual("Multiplex"); + expect(deviceDropdown.options[4].label).toEqual("MultiplexAndCovidOnly"); + + // Change device type to a multiplex device + await userEvent.selectOptions(deviceDropdown, device4Name); + + expect(screen.getByText("Flu A result")).toBeInTheDocument(); + expect(screen.getByText("Flu B result")).toBeInTheDocument(); + }); + }); }); diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index 5b1d073598..3dd67c296c 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -105,10 +105,10 @@ export const TestCard = ({ className={"margin-right-1"} onClick={removeTestFromQueue} > - Yes, I'm sure. + Yes, I'm sure - No, go back. + No, go back @@ -159,6 +159,7 @@ export const TestCard = ({ className={"card-close-button"} variant="unstyled" onClick={closeModalRef.current?.toggleModal} + ariaLabel={`Close test for ${patientFullName}`} > diff --git a/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap b/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap index 7ace5d2f2a..fa58ba6856 100644 --- a/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap +++ b/frontend/src/app/testQueue/TestCard/__snapshots__/TestCard.test.tsx.snap @@ -1,367 +1,318 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TestCard matches snapshot 1`] = ` -
    -
    -
  • +
    -
    -
    -
    - -
    -
    - +
    +
    - - - Dixon, Althea Hedda Mclaughlin - - - -
    -
    - - DOB: - 09/20/2015 - -
    -
    -
    - +
    +
    - - Start timer + DOB: + 09/20/2015 - - -
    -
    - + + + Start timer + + + +
    +
    + +
    -
    -
    -
    +
    -
    - + Current date and time + +
    - - - Swab of internal nose - - + + + + + + +
    -
    -
    -
    -
    -
    - - COVID-19 result + Specimen type - -
    + - -
    -
    +
    +
    +
    +
    +
    +
    +
    + + COVID-19 result + - Negative (-) - -
    + + * + +
    - - + + +
    +
    + + +
    +
    + + +
    -
    - + +
    -
    -
    -
    - - Is the patient pregnant? - -
    -
    - - -
    + Is the patient pregnant? +
    - - -
    -
    - -
    +
    + + +
    +
    - Prefer not to answer - + + +
    -
    - + +
    -
    -
    -
    - - Is the patient currently experiencing any symptoms? - -
    -
    - - -
    + Is the patient currently experiencing any symptoms? +
    - -
    +
    - No - + + +
    -
    - + +
  • -
    -
    - + +
    -
    - -
    -
    -
    + +
    +
    +
    , + "user": Object { + "clear": [Function], + "click": [Function], + "copy": [Function], + "cut": [Function], + "dblClick": [Function], + "deselectOptions": [Function], + "hover": [Function], + "keyboard": [Function], + "paste": [Function], + "pointer": [Function], + "selectOptions": [Function], + "setup": [Function], + "tab": [Function], + "tripleClick": [Function], + "type": [Function], + "unhover": [Function], + "upload": [Function], + }, +} `; diff --git a/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx b/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx index 16e00b0b2c..c1f65b8ef8 100644 --- a/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx +++ b/frontend/src/app/testQueue/TestCardForm/TestCardForm.tsx @@ -89,7 +89,7 @@ const TestCardForm = ({ covidAOEResponses: { pregnancy: testOrder.pregnancy as PregnancyCode, noSymptoms: testOrder.noSymptoms, - symptomOnsetDate: testOrder.symptomOnset, + symptomOnset: testOrder.symptomOnset, symptoms: testOrder.symptoms, }, }; @@ -188,9 +188,9 @@ const TestCardForm = ({ let debounceTimer: ReturnType; if (state.dirty) { dispatch({ type: TestFormActionCase.UPDATE_DIRTY_STATE, payload: false }); - debounceTimer = setTimeout(async () => { - await updateAOE(); - }, DEBOUNCE_TIME); + updateAOE(); + // debounceTimer = setTimeout(async () => { + // }, DEBOUNCE_TIME); } return () => { clearTimeout(debounceTimer); @@ -202,16 +202,17 @@ const TestCardForm = ({ if (whichAOEFormOption === AOEFormOptions.COVID) { trackUpdateAoEResponse(); try { - await updateAoeMutation({ + const newVar = { variables: { patientId: testOrder.patient.internalId, noSymptoms: state.covidAOEResponses.noSymptoms, symptoms: state.covidAOEResponses.symptoms, - symptomOnset: state.covidAOEResponses.symptomOnsetDate, + symptomOnset: state.covidAOEResponses.symptomOnset, pregnancy: state.covidAOEResponses.pregnancy, // testResultDelivery will now be determined by user preferences on backend }, - }); + }; + await updateAoeMutation(newVar); } catch (e) { // caught upstream by error boundary throw e; @@ -226,6 +227,13 @@ const TestCardForm = ({ (result) => result.diseaseName === MULTIPLEX_DISEASES.COVID_19 ); try { + console.log({ + id: testOrder.internalId, + deviceTypeId: state.deviceId, + dateTested: state.dateTested, + specimenTypeId: state.specimenId, + results: resultsToSave, + }); const response = await editQueueItem({ variables: { id: testOrder.internalId, @@ -287,9 +295,7 @@ const TestCardForm = ({ : ""; const testResultsError = validateTestResults(); - const showDateTestedError = - (hasDateTestedBeenTouched || hasAttemptedSubmit) && - dateTestedError.length > 0; + const showDateTestedError = dateTestedError.length > 0; const showTestResultsError = hasAttemptedSubmit && testResultsError.length > 0; @@ -432,12 +438,14 @@ const TestCardForm = ({ max={formatDate(moment().toDate())} value={formatDate(moment(state.dateTested).toDate())} onBlur={() => setHasDateTestedBeenTouched(true)} - onChange={(e) => - dispatch({ - type: TestFormActionCase.UPDATE_DATE_TESTED, - payload: e.target.value, - }) - } + onChange={(e) => { + if (!!e.target.value) { + dispatch({ + type: TestFormActionCase.UPDATE_DATE_TESTED, + payload: e.target.value, + }); + } + }} required={true} validationStatus={showDateTestedError ? "error" : undefined} errorMessage={showDateTestedError && dateTestedError} @@ -453,12 +461,15 @@ const TestCardForm = ({ data-testid="test-time" step="60" value={moment(state.dateTested).format("HH:mm")} - onChange={(e) => - dispatch({ - type: TestFormActionCase.UPDATE_TIME_TESTED, - payload: e.target.value, - }) - } + onChange={(e) => { + console.log("time I WAS HERE -----------", e.target.value); + if (!!e.target.value) { + dispatch({ + type: TestFormActionCase.UPDATE_TIME_TESTED, + payload: e.target.value, + }); + } + }} onBlur={() => setHasDateTestedBeenTouched(true)} validationStatus={showDateTestedError ? "error" : undefined} > @@ -469,7 +480,7 @@ const TestCardForm = ({
    {useCurrentTime && (