From dd038d1df8acf6ff581d567451eb3ba4d13f970f Mon Sep 17 00:00:00 2001 From: Bob Zhao Date: Wed, 16 Oct 2024 09:21:26 -0400 Subject: [PATCH] link together customize query --- .../tefca-viewer/e2e/query_workflow.spec.ts | 223 ++++---------- .../tefca-viewer/src/app/CustomQuery.ts | 2 +- containers/tefca-viewer/src/app/constants.ts | 20 +- .../app/query/components/CustomizeQuery.tsx | 17 +- .../query/components/PatientSearchResults.tsx | 77 ++--- .../src/app/query/components/ResultsView.tsx | 21 +- .../src/app/query/components/SelectQuery.tsx | 120 ++++++++ .../PatientSearchResultsTable.tsx | 9 +- .../resultsView/ResultsViewTable.tsx | 2 +- .../components/searchForm/SearchForm.tsx | 285 +++++++----------- .../searchForm/searchForm.module.css | 15 +- .../selectQuery/SelectSavedQuery.tsx | 137 +++++++++ .../components/selectQuery/queryHooks.ts | 108 +++++++ .../components/selectQuery/selectQuery.tsx | 167 ---------- .../src/app/query/designSystem/SiteAlert.tsx | 12 +- .../tefca-viewer/src/app/query/page.tsx | 141 +++------ .../tefca-viewer/src/app/query/test/page.tsx | 80 ----- .../src/styles/custom-styles.scss | 6 - .../tefca-viewer/src/styles/layout.scss | 2 +- 19 files changed, 669 insertions(+), 775 deletions(-) create mode 100644 containers/tefca-viewer/src/app/query/components/SelectQuery.tsx create mode 100644 containers/tefca-viewer/src/app/query/components/selectQuery/SelectSavedQuery.tsx create mode 100644 containers/tefca-viewer/src/app/query/components/selectQuery/queryHooks.ts delete mode 100644 containers/tefca-viewer/src/app/query/components/selectQuery/selectQuery.tsx delete mode 100644 containers/tefca-viewer/src/app/query/test/page.tsx diff --git a/containers/tefca-viewer/e2e/query_workflow.spec.ts b/containers/tefca-viewer/e2e/query_workflow.spec.ts index 5fd47b08f..65bb25f10 100644 --- a/containers/tefca-viewer/e2e/query_workflow.spec.ts +++ b/containers/tefca-viewer/e2e/query_workflow.spec.ts @@ -2,8 +2,20 @@ import { test, expect } from "@playwright/test"; import { TEST_URL } from "../playwright-setup"; - -test.describe("querying with the TryTEFCA viewer", () => { +import { STEP_ONE_PAGE_TITLE } from "@/app/query/components/searchForm/SearchForm"; +import { hyperUnluckyPatient } from "@/app/constants"; +import { + CONTACT_US_DISCLAIMER_EMAIL, + CONTACT_US_DISCLAIMER_TEXT, +} from "@/app/query/designSystem/SiteAlert"; +import { STEP_TWO_PAGE_TITLE } from "@/app/query/components/patientSearchResults/PatientSearchResultsTable"; +import { STEP_THREE_PAGE_TITLE } from "@/app/query/components/selectQuery/SelectSavedQuery"; + +const TEST_PATIENT = hyperUnluckyPatient; +const TEST_PATIENT_NAME = + hyperUnluckyPatient.FirstName + " A. " + hyperUnluckyPatient.LastName; + +test.describe("querying with the Query Connector", () => { test.beforeEach(async ({ page }) => { // Start every test on our main landing page await page.goto(TEST_URL); @@ -32,18 +44,9 @@ test.describe("querying with the TryTEFCA viewer", () => { test("unsuccessful user query: no patients", async ({ page }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); - await page - .getByLabel("Query", { exact: true }) - .selectOption("social-determinants"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("HELIOS Meld: Direct"); - - await page.getByLabel("First Name").fill("Ellie"); - await page.getByLabel("Last Name").fill("Williams"); - await page.getByLabel("Phone Number").fill("5555555555"); - await page.getByLabel("Medical Record Number").fill("TLOU1TLOU2"); + await page.getByRole("button", { name: "Fill fields" }).click(); + await page.getByLabel("First Name").fill("Shouldnt"); + await page.getByLabel("Last Name").fill("Findanyone"); await page.getByRole("button", { name: "Search for patient" }).click(); // Better luck next time, user! @@ -58,9 +61,7 @@ test.describe("querying with the TryTEFCA viewer", () => { .click(); }); - test("successful demo user query: the quest for watermelon mcgee", async ({ - page, - }) => { + test("successful demo user query", async ({ page }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); // Check that the info alert is visible and contains the correct text @@ -70,40 +71,42 @@ test.describe("querying with the TryTEFCA viewer", () => { "This site is for demo purposes only. Please do not enter PII on this website.", ); await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), + page.getByRole("heading", { name: STEP_ONE_PAGE_TITLE, exact: true }), ).toBeVisible(); - // Put in the search parameters for the elusive fruit person - await page - .getByLabel("Query", { exact: true }) - .selectOption("newborn-screening"); - await page - .getByLabel("Patient", { exact: true }) - .selectOption("newborn-screening-referral"); await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByLabel("First Name").fill("Watermelon"); - await page.getByLabel("Last Name").fill("McGee"); - await page.getByLabel("Date of Birth").fill("2024-07-12"); - await page.getByLabel("Medical Record Number").fill("18091"); - await page.getByLabel("Phone Number").fill("5555555555"); + await page.getByLabel("First Name").fill(TEST_PATIENT.FirstName); + await page.getByLabel("Last Name").fill(TEST_PATIENT.LastName); + await page.getByLabel("Date of Birth").fill(TEST_PATIENT.DOB); + await page.getByLabel("Medical Record Number").fill(TEST_PATIENT.MRN); + await page.getByLabel("Phone Number").fill(TEST_PATIENT.Phone); await page.getByRole("button", { name: "Search for patient" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await page.getByRole("link", { name: "Select patient" }).click(); + await expect( + page.getByRole("heading", { name: "Select a query" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + // Make sure we have a results page with a single patient // Non-interactive 'div' elements in the table should be located by text await expect( page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT_NAME)).toBeVisible(); await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); + await expect( + page.getByText(`Medical Record Number: ${TEST_PATIENT.MRN}`), + ).toBeVisible(); // Check that the info alert is visible and has updated to the correct text const alert2 = page.locator(".custom-alert"); await expect(alert2).toBeVisible(); await expect(alert2).toHaveText( - "Interested in learning more about using the TEFCA Query Connector for your jurisdiction? Send us an email at dibbs@cdc.gov", + `${CONTACT_US_DISCLAIMER_TEXT} ${CONTACT_US_DISCLAIMER_EMAIL}`, ); // Check to see if the accordion button is open @@ -117,7 +120,7 @@ test.describe("querying with the TryTEFCA viewer", () => { // Now let's use the return to search to go back to a blank form await page.getByRole("button", { name: "New patient search" }).click(); await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), + page.getByRole("heading", { name: STEP_ONE_PAGE_TITLE, exact: true }), ).toBeVisible(); }); @@ -125,11 +128,6 @@ test.describe("querying with the TryTEFCA viewer", () => { page, }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); - - await page.getByLabel("Query", { exact: true }).selectOption("syphilis"); - await page - .getByLabel("Patient", { exact: true }) - .selectOption("sti-syphilis-positive"); await page.getByRole("button", { name: "Fill fields" }).click(); // Delete last name and MRN to force phone number as one of the 3 fields @@ -140,25 +138,39 @@ test.describe("querying with the TryTEFCA viewer", () => { await page.getByRole("button", { name: "Search for patient" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); await expect( - page.getByRole("heading", { name: "Patient Record" }), + page.getByRole("heading", { name: STEP_TWO_PAGE_TITLE }), ).toBeVisible(); + await page.getByRole("link", { name: "Select patient" }).click(); + await expect( + page.getByRole("heading", { name: STEP_THREE_PAGE_TITLE }), + ).toBeVisible(); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("Hyper A. Unlucky")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT_NAME)).toBeVisible(); await expect(page.getByText("Contact")).toBeVisible(); - await expect(page.getByText("517-425-1398")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT.Phone)).toBeVisible(); await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("8692756")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT.MRN)).toBeVisible(); }); test("social determinants query with generalized function", async ({ page, }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); - await page - .getByLabel("Query", { exact: true }) - .selectOption("social-determinants"); await page.getByRole("button", { name: "Fill fields" }).click(); await page.getByRole("button", { name: "Search for patient" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + + await page.getByRole("link", { name: "Select patient" }).click(); + await expect( + page.getByRole("heading", { name: "Select a query" }), + ).toBeVisible(); + await page.getByTestId("Select").selectOption("social-determinants"); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await expect( page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); @@ -168,127 +180,16 @@ test.describe("querying with the TryTEFCA viewer", () => { page, }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); - await page.getByLabel("Query", { exact: true }).selectOption("chlamydia"); await page.getByRole("button", { name: "Fill fields" }).click(); await page.getByRole("button", { name: "Search for patient" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await page.getByRole("link", { name: "Select patient" }).click(); + await page.getByTestId("Select").selectOption("chlamydia"); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); await expect( page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); }); }); - -test.describe("Test the user journey of a 'tester'", () => { - test.beforeEach(async ({ page }) => { - // Start every test on direct tester page - await page.goto("http://localhost:3000/tefca-viewer/query/test", { - waitUntil: "load", - }); - }); - - test("query/test page loads", async ({ page }) => { - // Check that interactable elements are present - await expect( - page.getByRole("button", { name: "Data Usage Policy" }), - ).toBeVisible(); - await expect( - page.getByRole("link", { name: "TEFCA Viewer" }), - ).toBeVisible(); - - // Check that each expected text section is present - await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), - ).toBeVisible(); - await expect(page.getByLabel("Query", { exact: true })).toBeVisible(); - await expect(page.getByLabel("Patient", { exact: true })).toBeVisible(); - await expect(page.getByRole("button", { name: "Advanced" })).toBeVisible(); - }); - - test("Query for patient using auto-filled data", async ({ page }) => { - await page - .getByLabel("Query", { exact: true }) - .selectOption({ value: "newborn-screening" }); - await page - .getByLabel("Patient", { exact: true }) - .selectOption({ value: "newborn-screening-referral" }); - await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByRole("button", { name: "Search for patient" }).click(); - - // Make sure we have a results page with a single patient - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); - await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); - }); - - test("Query for patient by filling in data", async ({ page }) => { - await page - .getByLabel("Query", { exact: true }) - .selectOption("Newborn screening follow-up"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("HELIOS Meld: Direct"); - await page.getByLabel("First Name").fill("Watermelon"); - await page.getByLabel("Last Name").fill("McGee"); - await page.getByLabel("Phone Number").fill("5555555555"); - await page.getByLabel("Date of Birth").fill("2024-07-12"); - await page.getByLabel("Medical Record Number").fill("18091"); - - await page.getByRole("button", { name: "Search for patient" }).click(); - - // Make sure we have a results page with a single patient - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); - await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); - }); - - test("Query with multiple patients returned", async ({ page }) => { - // Query for a patient with multiple results - await page - .getByLabel("Query", { exact: true }) - .selectOption("Chlamydia case investigation"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("JMC Meld: Direct"); - await page.getByLabel("Last Name").fill("JMC"); - - await page.getByRole("button", { name: "Search for patient" }).click(); - // Make sure all the elements for the multiple patients view appear - await expect( - page.getByRole("heading", { name: "Select a patient" }), - ).toBeVisible(); - // Check that there is a Table element with the correct headers - await expect(page.locator("thead").locator("tr")).toHaveText( - "NameDOBContactAddressMRNActions", - ); - - // Check that there are multiple rows in the table - await expect(page.locator("tbody").locator("tr")).toHaveCount(10); - - // Click on the first patient's "Select patient" button - await page.locator(':nth-match(:text("Select patient"), 1)').click(); - - // Make sure we have a results page with a single patient & appropriate back buttons - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "New patient search" }), - ).toBeVisible(); - - await page.getByRole("button", { name: "New patient search" }).click(); - await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), - ).toBeVisible(); - }); -}); diff --git a/containers/tefca-viewer/src/app/CustomQuery.ts b/containers/tefca-viewer/src/app/CustomQuery.ts index beb1396cb..3f72897bb 100644 --- a/containers/tefca-viewer/src/app/CustomQuery.ts +++ b/containers/tefca-viewer/src/app/CustomQuery.ts @@ -75,7 +75,7 @@ export class CustomQuery { : ""; this.medicationRequestQuery = rxnormFilter !== "" - ? `/MedicationRequest?subject=${patientId}&code=${rxnormFilter}&_include=MedicationRequest:medication&_include=MedicationRequest:medication.administration` + ? `/MedicationRequest?subject=${patientId}&code=${rxnormFilter}&_include=MedicationRequest:medication&_include=MedicationRequest:intended-performer` : ""; this.socialHistoryQuery = `/Observation?subject=${patientId}&category=social-history`; this.encounterQuery = diff --git a/containers/tefca-viewer/src/app/constants.ts b/containers/tefca-viewer/src/app/constants.ts index d61ed2410..a003da268 100644 --- a/containers/tefca-viewer/src/app/constants.ts +++ b/containers/tefca-viewer/src/app/constants.ts @@ -37,6 +37,14 @@ export const demoQueryOptions = [ { value: "syphilis", label: "Syphilis case investigation" }, ]; +type DemoQueryOptionValue = (typeof demoQueryLabels)[number]; +export const demoQueryValToLabelMap = demoQueryOptions.reduce( + (acc, curVal) => { + acc[curVal.value as DemoQueryOptionValue] = curVal.label; + return acc; + }, + {} as Record, +); /* * Map between the queryType property used to define a demo use case's options, * and the name of that query for purposes of searching the DB. @@ -90,16 +98,17 @@ export type PatientType = | "social-determinants" | "sti-syphilis-positive"; +export const DEFAULT_DEMO_FHIR_SERVER = "Public HAPI: Direct"; /* * Common "Hyper Unlucky" patient data used for all non-newborn screening use cases */ -const hyperUnluckyPatient: DemoDataFields = { +export const hyperUnluckyPatient: DemoDataFields = { FirstName: "Hyper", LastName: "Unlucky", DOB: "1975-12-06", MRN: "8692756", Phone: "517-425-1398", - FhirServer: "Public HAPI: Direct", + FhirServer: DEFAULT_DEMO_FHIR_SERVER, UseCase: "cancer", // UseCase will be updated per case }; @@ -276,12 +285,7 @@ export const stateOptions = [ ]; /* Mode that pages can be in; determines what is displayed to the user */ -export type Mode = - | "search" - | "results" - | "customize-queries" - | "select-query" - | "patient-results"; +export type Mode = "search" | "results" | "select-query" | "patient-results"; /*Type to specify the expected components for each item in a value set that will be displayed in the CustomizeQuery component*/ diff --git a/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx b/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx index 7e4e62a78..a8a6f4770 100644 --- a/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx +++ b/containers/tefca-viewer/src/app/query/components/CustomizeQuery.tsx @@ -2,7 +2,12 @@ import React, { useState, useEffect } from "react"; import { Button } from "@trussworks/react-uswds"; -import { ValueSetType, ValueSetItem } from "../../constants"; +import { + ValueSetType, + ValueSetItem, + USE_CASES, + demoQueryValToLabelMap, +} from "../../constants"; import { UseCaseQueryResponse } from "@/app/query-service"; import LoadingView from "./LoadingView"; import { showRedirectConfirmation } from "../designSystem/redirectToast/RedirectToast"; @@ -16,7 +21,7 @@ import Backlink from "./backLink/Backlink"; interface CustomizeQueryProps { useCaseQueryResponse: UseCaseQueryResponse; - queryType: string; + queryType: USE_CASES; queryValuesets: ValueSetItem[]; setQueryValuesets: (queryVS: ValueSetItem[]) => void; goBack: () => void; @@ -125,11 +130,7 @@ const CustomizeQuery: React.FC = ({ const handleApplyChanges = () => { const selectedItems = Object.keys(valueSetOptions).reduce((acc, key) => { const items = valueSetOptions[key as ValueSetType]; - acc = acc.concat( - Object.values(items) - .flatMap((dict) => dict.items) - .filter((item) => item.include), - ); + acc = acc.concat(Object.values(items).flatMap((dict) => dict.items)); return acc; }, [] as ValueSetItem[]); setQueryValuesets(selectedItems); @@ -165,7 +166,7 @@ const CustomizeQuery: React.FC = ({ Customize query
- Query: {queryType} + Query: {demoQueryValToLabelMap[queryType]}
{countLabs} labs found, {countMedications} medications found,{" "} diff --git a/containers/tefca-viewer/src/app/query/components/PatientSearchResults.tsx b/containers/tefca-viewer/src/app/query/components/PatientSearchResults.tsx index ee41abc7f..5d01f6e76 100644 --- a/containers/tefca-viewer/src/app/query/components/PatientSearchResults.tsx +++ b/containers/tefca-viewer/src/app/query/components/PatientSearchResults.tsx @@ -1,12 +1,7 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { Patient } from "fhir/r4"; -import { - UseCaseQueryResponse, - UseCaseQuery, - UseCaseQueryRequest, -} from "../../query-service"; -import { Mode, ValueSetItem } from "@/app/constants"; +import { Mode } from "@/app/constants"; import Backlink from "./backLink/Backlink"; import PatientSearchResultsTable from "./patientSearchResults/PatientSearchResultsTable"; import NoPatientsFound from "./patientSearchResults/NoPatientsFound"; @@ -16,94 +11,58 @@ import NoPatientsFound from "./patientSearchResults/NoPatientsFound"; */ export interface PatientSearchResultsProps { patients: Patient[]; - originalRequest: UseCaseQueryRequest; - queryValueSets: ValueSetItem[]; - setLoading: (loading: boolean) => void; goBack: () => void; setMode: (mode: Mode) => void; - setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void; + setPatientForQueryResponse: (patient: Patient) => void; } /** * Displays multiple patient search results in a table. * @param root0 - PatientSearchResults props. * @param root0.patients - The array of Patient resources. - * @param root0.originalRequest - The original request object. - * @param root0.queryValueSets - The stateful collection of value sets to include - * in the query. - * @param root0.setLoading - The function to set the loading state. * @param root0.goBack - The function to go back to the previous page. - * @param root0.setUseCaseQueryResponse - State update function to pass the - * data needed for the results view back up to the parent component * @param root0.setMode - Redirect function to handle results view routing + * @param root0.setPatientForQueryResponse - Callback function to update the + * patient being searched for * @returns - The PatientSearchResults component. */ const PatientSearchResults: React.FC = ({ patients, - originalRequest, - queryValueSets, - setLoading, goBack, - setUseCaseQueryResponse, + setPatientForQueryResponse, setMode, }) => { useEffect(() => { window.scrollTo(0, 0); }, []); - const [patientForQuery, setPatientForQueryResponse] = useState(); - useEffect(() => { - let isSubscribed = true; - - const fetchQuery = async () => { - if (patientForQuery && isSubscribed) { - setLoading(true); - const queryResponse = await UseCaseQuery( - originalRequest, - queryValueSets, - { - Patient: [patientForQuery], - }, - ); - setUseCaseQueryResponse(queryResponse); - setMode("results"); - setLoading(false); - } - }; - - fetchQuery().catch(console.error); - - // Destructor hook to prevent future state updates - return () => { - isSubscribed = false; - }; - }, [patientForQuery]); + function handlePatientSelect(patient: Patient) { + setPatientForQueryResponse(patient); + setMode("select-query"); + } return ( <> - {patients.length === 0 && ( <> - + Revise your patient search )} {patients.length > 0 && ( <> + + - -

- Not seeing what you're looking for? -

- - - Return to patient search - )} diff --git a/containers/tefca-viewer/src/app/query/components/ResultsView.tsx b/containers/tefca-viewer/src/app/query/components/ResultsView.tsx index 23ad78470..ac1debfc6 100644 --- a/containers/tefca-viewer/src/app/query/components/ResultsView.tsx +++ b/containers/tefca-viewer/src/app/query/components/ResultsView.tsx @@ -11,11 +11,14 @@ import DiagnosticReportTable from "./resultsView/tableComponents/DiagnosticRepor import EncounterTable from "./resultsView/tableComponents/EncounterTable"; import MedicationRequestTable from "./resultsView/tableComponents/MedicationRequestTable"; import ObservationTable from "./resultsView/tableComponents/ObservationTable"; +import Backlink from "./backLink/Backlink"; +import { USE_CASES, UseCaseToQueryName } from "@/app/constants"; type ResultsViewProps = { useCaseQueryResponse: UseCaseQueryResponse; + selectedQuery: USE_CASES; goBack: () => void; - queryName: string; + goToBeginning: () => void; }; export type ResultsViewAccordionItem = { @@ -29,13 +32,15 @@ export type ResultsViewAccordionItem = { * @param props - The props for the QueryView component. * @param props.useCaseQueryResponse - The response from the query service. * @param props.goBack - The function to go back to the previous page. - * @param props.queryName - The name of the saved query to display to the user + * @param props.goToBeginning - Function to return to patient discover + * @param props.selectedQuery - query that's been selected to view for results * @returns The QueryView component. */ const ResultsView: React.FC = ({ useCaseQueryResponse, + selectedQuery, goBack, - queryName, + goToBeginning, }) => { useEffect(() => { window.scrollTo(0, 0); @@ -56,9 +61,13 @@ const ResultsView: React.FC = ({ <>
+ goBack()} + label={"Return to query selection"} + /> @@ -70,7 +79,9 @@ const ResultsView: React.FC = ({

Query:{" "} - {queryName} + + {UseCaseToQueryName[selectedQuery]} +

diff --git a/containers/tefca-viewer/src/app/query/components/SelectQuery.tsx b/containers/tefca-viewer/src/app/query/components/SelectQuery.tsx new file mode 100644 index 000000000..5843985eb --- /dev/null +++ b/containers/tefca-viewer/src/app/query/components/SelectQuery.tsx @@ -0,0 +1,120 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { FHIR_SERVERS, USE_CASES, ValueSetItem } from "../../constants"; +import CustomizeQuery from "./CustomizeQuery"; +import SelectSavedQuery from "./selectQuery/SelectSavedQuery"; + +import { QueryResponse } from "@/app/query-service"; +import { Patient } from "fhir/r4"; +import { + fetchQueryResponse, + fetchUseCaseValueSets, +} from "./selectQuery/queryHooks"; +import LoadingView from "./LoadingView"; + +interface SelectQueryProps { + goForward: () => void; + goBack: () => void; + selectedQuery: USE_CASES; + setSelectedQuery: React.Dispatch>; + patientForQuery: Patient | undefined; + resultsQueryResponse: QueryResponse; + setResultsQueryResponse: React.Dispatch>; + fhirServer: FHIR_SERVERS; + setFhirServer: React.Dispatch>; + setLoading: (isLoading: boolean) => void; +} + +/** + * @param root0 - SelectQueryProps + * @param root0.goBack - Callback to return to previous page + * @param root0.goForward - Callback to go to the next page + * @param root0.selectedQuery - query we chose for further customization + * @param root0.setSelectedQuery - callback function to update the selected query + * @param root0.patientForQuery - patient to apply a particular query for + * @param root0.resultsQueryResponse - Response of selected query + * @param root0.setResultsQueryResponse - Callback function to update selected + * query + * @param root0.fhirServer - the FHIR server that we're running the query against + * @param root0.setFhirServer - callback function to update the FHIR server + * @returns - The selectQuery component. + */ +const SelectQuery: React.FC = ({ + selectedQuery, + patientForQuery, + resultsQueryResponse, + fhirServer, + goForward, + goBack, + setSelectedQuery, + setResultsQueryResponse, + setFhirServer, +}) => { + const [showCustomizeQuery, setShowCustomizedQuery] = useState(false); + const [queryValueSets, setQueryValueSets] = useState( + [] as ValueSetItem[], + ); + const [loadingQueryValueSets, setLoadingQueryValueSets] = + useState(false); + + const [loadingResultResponse, setLoadingResultResponse] = + useState(false); + + useEffect(() => { + // Gate whether we actually update state after fetching so we + // avoid name-change race conditions + let isSubscribed = true; + + fetchUseCaseValueSets( + selectedQuery, + setQueryValueSets, + isSubscribed, + setLoadingQueryValueSets, + ).catch(console.error); + + // Destructor hook to prevent future state updates + return () => { + isSubscribed = false; + }; + }, [selectedQuery, setQueryValueSets]); + + async function onSubmit() { + await fetchQueryResponse({ + patientForQuery: patientForQuery, + selectedQuery: selectedQuery, + queryValueSets: queryValueSets, + fhirServer: fhirServer, + queryResponseStateCallback: setResultsQueryResponse, + setIsLoading: setLoadingResultResponse, + }).catch(console.error); + goForward(); + } + + return ( + <> + {showCustomizeQuery ? ( + setShowCustomizedQuery(false)} + > + ) : ( + + )} + {loadingResultResponse && } + + ); +}; + +export default SelectQuery; +export const RETURN_TO_STEP_ONE_LABEL = "Return to Select patient"; diff --git a/containers/tefca-viewer/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx b/containers/tefca-viewer/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx index 53fabf08b..59bdf38b7 100644 --- a/containers/tefca-viewer/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx +++ b/containers/tefca-viewer/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx @@ -9,7 +9,7 @@ import { type PatientSeacrchResultsTableProps = { patients: Patient[]; - setPatientForQueryResponse: (patient: Patient) => void; + handlePatientSelect: (patient: Patient) => void; }; /** @@ -17,18 +17,18 @@ type PatientSeacrchResultsTableProps = { * include in their query * @param param0 - props * @param param0.patients - Patient[] from the FHIR spec to display as rows - * @param param0.setPatientForQueryResponse - state setter function to redirect + * @param param0.handlePatientSelect - state setter function to redirect * to the results view * @returns The patient search results view */ const PatientSearchResultsTable: React.FC = ({ patients, - setPatientForQueryResponse, + handlePatientSelect: setPatientForQueryResponse, }) => { return ( <>

- Select a patient + {STEP_TWO_PAGE_TITLE}

The following records match your search. Select a patient to continue. @@ -74,3 +74,4 @@ const PatientSearchResultsTable: React.FC = ({ }; export default PatientSearchResultsTable; +export const STEP_TWO_PAGE_TITLE = "Step 2: Select a patient"; diff --git a/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx index 5481ba517..b828b46c3 100644 --- a/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx +++ b/containers/tefca-viewer/src/app/query/components/resultsView/ResultsViewTable.tsx @@ -57,5 +57,5 @@ export default ResultsViewTable; * @returns - A hyphenated id that can be linked as an anchor tag */ export function formatIdForAnchorTag(title: string) { - return title.toLocaleLowerCase().replace(" ", "-"); + return title?.toLocaleLowerCase().replace(" ", "-"); } diff --git a/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx b/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx index aedac810e..cc12ab403 100644 --- a/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx +++ b/containers/tefca-viewer/src/app/query/components/searchForm/SearchForm.tsx @@ -10,18 +10,10 @@ import { USE_CASES, FHIR_SERVERS, demoData, - PatientType, - demoQueryOptions, - patientOptions, stateOptions, Mode, - ValueSetItem, } from "../../../constants"; -import { - UseCaseQueryResponse, - UseCaseQuery, - UseCaseQueryRequest, -} from "../../../query-service"; +import { UseCaseQueryResponse, UseCaseQuery } from "../../../query-service"; import { fhirServers } from "../../../fhir-servers"; import styles from "./searchForm.module.css"; @@ -29,45 +21,41 @@ import { FormatPhoneAsDigits } from "@/app/format-service"; interface SearchFormProps { useCase: USE_CASES; - queryValueSets: ValueSetItem[]; setUseCase: (useCase: USE_CASES) => void; - setOriginalRequest: (originalRequest: UseCaseQueryRequest) => void; - setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void; + setPatientDiscoveryQueryResponse: ( + UseCaseQueryResponse: UseCaseQueryResponse, + ) => void; setMode: (mode: Mode) => void; setLoading: (loading: boolean) => void; - setQueryType: (queryType: string) => void; + fhirServer: FHIR_SERVERS; + setFhirServer: React.Dispatch>; } /** * @param root0 - SearchFormProps * @param root0.useCase - The use case this query will cover. - * @param root0.queryValueSets - Stateful collection of valuesets to use in the query. * @param root0.setUseCase - Update stateful use case. - * @param root0.setOriginalRequest - The function to set the original request. - * @param root0.setUseCaseQueryResponse - The function to set the use case query response. * @param root0.setMode - The function to set the mode. * @param root0.setLoading - The function to set the loading state. - * @param root0.setQueryType - The function to set the query type. + * @param root0.setPatientDiscoveryQueryResponse - callback function to set the + * patient for use in future steps + * @param root0.fhirServer - server to do the query against + * @param root0.setFhirServer - callback function to update specified query * @returns - The SearchForm component. */ const SearchForm: React.FC = ({ useCase, - queryValueSets, setUseCase, - setOriginalRequest, - setUseCaseQueryResponse, + setPatientDiscoveryQueryResponse, setMode, setLoading, - setQueryType, + fhirServer, + setFhirServer, }) => { //Set the patient options based on the demoOption - const [patientOption, setPatientOption] = useState( - patientOptions[useCase]?.[0]?.value || "", - ); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); - const [fhirServer, setFhirServer] = useState(); - // "Public HAPI: Direct", + const [phone, setPhone] = useState(""); const [dob, setDOB] = useState(""); const [mrn, setMRN] = useState(""); @@ -75,10 +63,10 @@ const SearchForm: React.FC = ({ const [showAdvanced, setShowAdvanced] = useState(false); const [autofilled, setAutofilled] = useState(false); // boolean indicating if the form was autofilled, changes color if true - // Fills fields with sample data based on the selected patientOption + // Fills fields with sample data based on the selected const fillFields = useCallback( - (patientOption: PatientType, highlightAutofilled = true) => { - const data = demoData[patientOption]; + (highlightAutofilled = true) => { + const data = demoData["cancer"]; if (data) { setFirstName(data.FirstName); setLastName(data.LastName); @@ -90,23 +78,9 @@ const SearchForm: React.FC = ({ setAutofilled(highlightAutofilled); } }, - [patientOption, setUseCase, setQueryType], + [setUseCase], ); - // Change the selectedDemoOption in the dropdown and update the - // query type (which governs the DB fetch) accordingly - const handleDemoQueryChange = (selectedDemoOption: string) => { - setPatientOption(patientOptions[selectedDemoOption][0].value); - setQueryType( - demoQueryOptions.find((dqo) => dqo.value == selectedDemoOption)?.label || - "", - ); - }; - - const handleClick = () => { - setMode("customize-queries"); - }; - async function HandleSubmit(event: React.FormEvent) { event.preventDefault(); if (!useCase || !fhirServer) { @@ -124,15 +98,10 @@ const SearchForm: React.FC = ({ use_case: useCase, phone: FormatPhoneAsDigits(phone), }; - setOriginalRequest(originalRequest); - const queryResponse = await UseCaseQuery(originalRequest, queryValueSets); - setUseCaseQueryResponse(queryResponse); + const queryResponse = await UseCaseQuery(originalRequest, []); + setPatientDiscoveryQueryResponse(queryResponse); - if (queryResponse.Patient && queryResponse.Patient.length === 1) { - setMode("results"); - } else { - setMode("patient-results"); - } + setMode("patient-results"); setLoading(false); } useEffect(() => { @@ -143,7 +112,7 @@ const SearchForm: React.FC = ({ <>

- Search for a Patient + {STEP_ONE_PAGE_TITLE}

Enter patient information below to search for a patient. We will query @@ -156,76 +125,22 @@ const SearchForm: React.FC = ({ htmlFor="query" > The demo site uses synthetic data to provide examples of possible - queries that you can make with the TEFCA Viewer. Select a query + queries that you can make with the Query Connector. Select a query use case, a sample patient, and then click “fill fields” below. -
-
- -
- -
-
-
- -
- -
-
-
-

} -
+
{showAdvanced && ( -
-