From b515d212fcc6c47f382d1bb42c2f0207d8fce87a Mon Sep 17 00:00:00 2001 From: AJAL ODORA JONATHAN <43242517+ODORA0@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:32:32 +0300 Subject: [PATCH 1/2] OHRI-2280 Latest HIV Test Result displayed on patient banner --- .../banner-tags/.patient-status-tag.test.tsx | 83 +++++++++-- .../patient-status-tag.component.tsx | 20 +-- .../banner-tags/patientHivStatus.ts | 130 ++++++++++++++---- .../banner-tags/patientStatus.test.ts | 52 +++++-- packages/esm-commons-lib/src/config.schema.ts | 25 ++++ packages/esm-commons-lib/src/index.ts | 12 +- packages/esm-commons-lib/src/routes.json | 8 +- 7 files changed, 264 insertions(+), 66 deletions(-) create mode 100644 packages/esm-commons-lib/src/config.schema.ts diff --git a/packages/esm-commons-lib/src/components/banner-tags/.patient-status-tag.test.tsx b/packages/esm-commons-lib/src/components/banner-tags/.patient-status-tag.test.tsx index d1f7dc74a..b371a66b2 100644 --- a/packages/esm-commons-lib/src/components/banner-tags/.patient-status-tag.test.tsx +++ b/packages/esm-commons-lib/src/components/banner-tags/.patient-status-tag.test.tsx @@ -2,35 +2,90 @@ import React from 'react'; import { render, act, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { PatientStatusBannerTag } from './patient-status-tag.component'; -import { isPatientHivPositive } from './patientHivStatus'; +import { usePatientHivStatus } from './patientHivStatus'; -const mockIsPatientHivPositive = isPatientHivPositive as jest.Mock; jest.mock('./patientHivStatus'); +const mockusePatientHivStatus = usePatientHivStatus as jest.Mock; + describe('PatientStatusBannerTag', () => { beforeEach(() => { jest.clearAllMocks(); }); - const hivPositiveSampleUuid = '703AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const hivPositiveSampleUuid = '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + + it('renders red tag when patient is HIV positive', async () => { + mockusePatientHivStatus.mockReturnValue({ + hivStatus: 'positive', + isLoading: false, + isError: false, + }); + + await act(async () => { + render(); + }); + + expect(screen.getByText(/HIV Positive/i)).toBeInTheDocument(); + }); - describe('PatientStatusBannerTag', () => { - it('renders red tag when patient is HIV positive', async () => { - mockIsPatientHivPositive.mockResolvedValue(true); - await act(async () => { - render(); - }); + it('renders green tag when patient is HIV negative', async () => { + mockusePatientHivStatus.mockReturnValue({ + hivStatus: 'negative', + isLoading: false, + isError: false, + }); - expect(screen.getByText(/HIV Positive/i)).toBeInTheDocument(); + await act(async () => { + render(); }); + + expect(screen.getByText(/HIV Negative/i)).toBeInTheDocument(); }); - it('does not render red tag when patient is not HIV positive', async () => { + it('does not render any tag when patient HIV status is not positive or negative', async () => { + mockusePatientHivStatus.mockReturnValue({ + hivStatus: 'other', + isLoading: false, + isError: false, + }); + + await act(async () => { + render(); + }); + + expect(screen.queryByText(/HIV Positive/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/HIV Negative/i)).not.toBeInTheDocument(); + }); + + it('shows loading state initially', async () => { + mockusePatientHivStatus.mockReturnValue({ + hivStatus: null, + isLoading: true, + isError: false, + }); + + await act(async () => { + render(); + }); + + expect(screen.queryByText(/HIV Positive/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/HIV Negative/i)).not.toBeInTheDocument(); + }); + + it('handles error state', async () => { + mockusePatientHivStatus.mockReturnValue({ + hivStatus: null, + isLoading: false, + isError: true, + }); + await act(async () => { - (isPatientHivPositive as jest.Mock).mockResolvedValue(false); - render(); + render(); }); - expect(screen.queryByText('HIV Positive')).not.toBeInTheDocument(); + expect(screen.queryByText(/HIV Positive/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/HIV Negative/i)).not.toBeInTheDocument(); + // Optionally check for an error message if your component shows one }); }); diff --git a/packages/esm-commons-lib/src/components/banner-tags/patient-status-tag.component.tsx b/packages/esm-commons-lib/src/components/banner-tags/patient-status-tag.component.tsx index 8c6a6368b..1ef075df7 100644 --- a/packages/esm-commons-lib/src/components/banner-tags/patient-status-tag.component.tsx +++ b/packages/esm-commons-lib/src/components/banner-tags/patient-status-tag.component.tsx @@ -1,18 +1,18 @@ import React, { useEffect, useState } from 'react'; import { Tag } from '@carbon/react'; import { useTranslation } from 'react-i18next'; -import { isPatientHivPositive } from './patientHivStatus'; +import { usePatientHivStatus } from './patientHivStatus'; export function PatientStatusBannerTag({ patientUuid }) { const { t } = useTranslation(); - const [hivPositive, setHivPositive] = useState(false); + const { hivStatus } = usePatientHivStatus(patientUuid); - useEffect(() => { - isPatientHivPositive(patientUuid).then((result) => setHivPositive(result)); - }, [hivPositive, patientUuid]); - - //TODO: Improve refresh time - // forceRerender(); - - return <>{hivPositive && {t('hivPositive', 'HIV Positive')}}; + return ( + <> + {hivStatus === 'positive' && {t('hivPositive', 'HIV Positive')}} + {hivStatus === 'negative' && {t('hivNegative', 'HIV Negative')}} + + ); } + +export default PatientStatusBannerTag; diff --git a/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts b/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts index c3df0b78e..2336607cb 100644 --- a/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts +++ b/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts @@ -1,44 +1,116 @@ -import { openmrsFetch } from '@openmrs/esm-framework'; +import { openmrsFetch, useConfig } from '@openmrs/esm-framework'; import { fetchPatientsFinalHIVStatus, fetchPatientComputedConcept_HIV_Status } from '../../api.resource'; +import { useState, useEffect } from 'react'; -const fetchPatientHtsEncounters = (patientUuid: string) => { - const htsEncounterRepresentation = - 'custom:(uuid,encounterDatetime,location:(uuid,name),' + - 'encounterProviders:(uuid,provider:(uuid,name)),' + - 'obs:(uuid,obsDatetime,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name))))'; - const htsRetrospectiveTypeUUID = '79c1f50f-f77d-42e2-ad2a-d29304dde2fe'; - const query = `encounterType=${htsRetrospectiveTypeUUID}&patient=${patientUuid}`; +const usePatientHtsEncounters = (patientUuid: string) => { + const [encounters, setEncounters] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const config = useConfig(); - return openmrsFetch(`/ws/rest/v1/encounter?${query}&v=${htsEncounterRepresentation}`); + useEffect(() => { + const fetchEncounters = async () => { + const htsEncounterRepresentation = + 'custom:(uuid,encounterDatetime,location:(uuid,name),' + + 'encounterProviders:(uuid,provider:(uuid,name)),' + + 'obs:(uuid,obsDatetime,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name))))'; + const antenatalEncounterType = config.encounterTypes.antenatalEncounterType; + + if (!antenatalEncounterType) { + setIsError(true); + setIsLoading(false); + return; + } + + const query = `encounterType=${antenatalEncounterType}&patient=${patientUuid}`; + + try { + const response = await openmrsFetch(`/ws/rest/v1/encounter?${query}&v=${htsEncounterRepresentation}`); + setEncounters(response.data.results); + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + if (patientUuid) { + fetchEncounters(); + } + }, [patientUuid, config]); + + return { encounters, isLoading, isError }; }; -const isPatientHivPositive = async (patientUuid: string) => { - const hivTestResultConceptUUID = 'de18a5c1-c187-4698-9d75-258605ea07e8'; // Concept: Result of HIV test +const usePatientHivStatus = (patientUuid: string) => { + const [hivStatus, setHivStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const config = useConfig(); + const { encounters, isLoading: encountersLoading, isError: encountersError } = usePatientHtsEncounters(patientUuid); - let isHivPositive = false; - let htsTestResult; + useEffect(() => { + const fetchHivStatus = async () => { + const hivTestResultConceptUUID = config.obsConcepts.hivTestResultConceptUUID; + const positiveUUID = config.obsConcepts.positiveUUID; + const negativeUUID = config.obsConcepts.negativeUUID; - await fetchPatientHtsEncounters(patientUuid).then((encounters) => { - encounters.data.results.forEach((encounter) => { - htsTestResult = encounter.obs.find((observation) => observation.concept.name.uuid === hivTestResultConceptUUID); + let hivStatus = ''; - if (htsTestResult && htsTestResult.value.name.uuid === 'ade5ba3f-3c7f-42b1-96d1-cfeb9b446980') { - isHivPositive = true; + if (encountersError) { + setIsError(true); + setIsLoading(false); + return; } - }); - }); - const hivFinalStatus = await fetchPatientsFinalHIVStatus(patientUuid); + if (!encountersLoading) { + try { + encounters.forEach((encounter) => { + const htsTestResult = encounter.obs.find( + (observation) => observation.concept.uuid === hivTestResultConceptUUID, + ); + + if (htsTestResult) { + if (htsTestResult.value.uuid === positiveUUID) { + hivStatus = 'positive'; + } else if (htsTestResult.value.uuid === negativeUUID) { + hivStatus = 'negative'; + } + } + }); + + if (!hivStatus) { + const hivFinalStatus = await fetchPatientsFinalHIVStatus(patientUuid); + const computedConcept = await fetchPatientComputedConcept_HIV_Status(patientUuid); - const computedConcept = await fetchPatientComputedConcept_HIV_Status(patientUuid); + if ( + hivFinalStatus.toLowerCase().includes('positive') || + computedConcept.toLowerCase().includes('positive') + ) { + hivStatus = 'positive'; + } else if ( + hivFinalStatus.toLowerCase().includes('negative') || + computedConcept.toLowerCase().includes('negative') + ) { + hivStatus = 'negative'; + } + } + + setHivStatus(hivStatus); + } catch (error) { + setIsError(true); + } finally { + setIsLoading(false); + } + } + }; - if (hivFinalStatus.toLowerCase().includes('positive') || computedConcept.toLowerCase().includes('positive')) { - isHivPositive = true; - } else { - isHivPositive = false; - } + if (patientUuid) { + fetchHivStatus(); + } + }, [patientUuid, encountersLoading, encountersError, encounters, config]); - return isHivPositive; + return { hivStatus, isLoading, isError }; }; -export { isPatientHivPositive }; +export { usePatientHtsEncounters, usePatientHivStatus }; diff --git a/packages/esm-commons-lib/src/components/banner-tags/patientStatus.test.ts b/packages/esm-commons-lib/src/components/banner-tags/patientStatus.test.ts index 686e2f835..19d053983 100644 --- a/packages/esm-commons-lib/src/components/banner-tags/patientStatus.test.ts +++ b/packages/esm-commons-lib/src/components/banner-tags/patientStatus.test.ts @@ -1,16 +1,48 @@ -/** - * @jest-environment jsdom - */ +import { useConfig } from '@openmrs/esm-framework'; +import { usePatientHivStatus, usePatientHtsEncounters } from './patientHivStatus'; +import { renderHook } from '@testing-library/react'; -import { isPatientHivPositive } from './patientHivStatus'; +jest.mock('@openmrs/esm-framework', () => ({ + openmrsFetch: jest.fn(), + useConfig: jest.fn(), +})); -describe('Patient HIV Status', () => { - it('Should return positive', () => { - let isHivPositive; - isPatientHivPositive('b280078a-c0ce-443b-9997-3c66c63ec2f8').then((result) => { - isHivPositive = result; +jest.mock('../../api.resource', () => ({ + fetchPatientsFinalHIVStatus: jest.fn(), + fetchPatientComputedConcept_HIV_Status: jest.fn(), +})); - expect(isHivPositive).toBe(true); +const mockUseConfig = useConfig as jest.Mock; + +describe('usePatientHtsEncounters', () => { + it('should return loading state initially', () => { + mockUseConfig.mockReturnValue({ + encounterTypes: { antenatalEncounterType: '677d1a80-dbbe-4399-be34-aa7f54f11405' }, }); + + const { result } = renderHook(() => usePatientHtsEncounters('1a4d8ff9-a95f-4c18-9b24-a59bd40b3fc0')); + + expect(result.current.isLoading).toBe(true); + expect(result.current.isError).toBe(false); + expect(result.current.encounters).toEqual([]); + }); +}); + +describe('usePatientHivStatus', () => { + it('should return loading state initially', () => { + mockUseConfig.mockReturnValue({ + encounterTypes: { antenatalEncounterType: '677d1a80-dbbe-4399-be34-aa7f54f11405' }, + obsConcepts: { + hivTestResultConceptUUID: '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + positiveUUID: '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + negativeUUID: '664AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + }); + + const { result } = renderHook(() => usePatientHivStatus('1a4d8ff9-a95f-4c18-9b24-a59bd40b3fc0')); + + expect(result.current.isLoading).toBe(true); + expect(result.current.isError).toBe(false); + expect(result.current.hivStatus).toBe(null); }); }); diff --git a/packages/esm-commons-lib/src/config.schema.ts b/packages/esm-commons-lib/src/config.schema.ts new file mode 100644 index 000000000..433ddfb6e --- /dev/null +++ b/packages/esm-commons-lib/src/config.schema.ts @@ -0,0 +1,25 @@ +import { Type } from '@openmrs/esm-framework'; + +export const configSchema = { + obsConcepts: { + _type: Type.Object, + _description: 'List of observation concept UUIDs.', + _default: { + hivTestResultConceptUUID: '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + positiveUUID: '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + negativeUUID: '664AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + }, + encounterTypes: { + _type: Type.Object, + _description: 'List of encounter type UUIDs', + _default: { + antenatalEncounterType: '677d1a80-dbbe-4399-be34-aa7f54f11405', + }, + }, +}; + +export interface ConfigObject { + encounterTypes: Object; + obsConcepts: object; +} diff --git a/packages/esm-commons-lib/src/index.ts b/packages/esm-commons-lib/src/index.ts index f4982ca46..67755c5c3 100644 --- a/packages/esm-commons-lib/src/index.ts +++ b/packages/esm-commons-lib/src/index.ts @@ -1,5 +1,7 @@ import { FormEngine } from '@openmrs/openmrs-form-engine-lib'; -import { getSyncLifecycle } from '@openmrs/esm-framework'; +import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework'; +import { PatientStatusBannerTag } from './components/banner-tags/patient-status-tag.component'; +import { configSchema } from './config.schema'; export * from './constants'; export * from './api.resource'; @@ -62,5 +64,13 @@ const options = { moduleName: '@ohri/openmrs-esm-ohri-commons-lib', }; +const moduleName = '@ohri/openmrs-esm-ohri-commons-lib'; + +export function startupApp() { + defineConfigSchema(moduleName, configSchema); +} + // t('ohriForms', "OHRI Forms") export const ohriFormsWorkspace = getSyncLifecycle(FormEngine, options); + +export const patientStatusBannerTagExtension = getSyncLifecycle(PatientStatusBannerTag, options); diff --git a/packages/esm-commons-lib/src/routes.json b/packages/esm-commons-lib/src/routes.json index 2bcf732e7..dc4ce73aa 100644 --- a/packages/esm-commons-lib/src/routes.json +++ b/packages/esm-commons-lib/src/routes.json @@ -3,9 +3,13 @@ "backendDependencies": { "webservices.rest": "^2.24.0" }, - "pages": [ - ], + "pages": [], "extensions": [ + { + "name": "patient-status-banner-tag", + "slot": "patient-banner-tags-slot", + "component": "patientStatusBannerTagExtension" + } ], "workspaces": [ { From 49dc6ac093d16808594482665e28f04d8023045a Mon Sep 17 00:00:00 2001 From: AJAL ODORA JONATHAN <43242517+ODORA0@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:04:22 +0300 Subject: [PATCH 2/2] Add configs to PMTCT --- .../src/components/banner-tags/patientHivStatus.ts | 10 ++++++---- packages/esm-ohri-pmtct-app/src/config-schema.ts | 3 +++ packages/esm-ohri-pmtct-app/src/index.ts | 3 +++ packages/esm-ohri-pmtct-app/src/routes.json | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts b/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts index 2336607cb..87b17715a 100644 --- a/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts +++ b/packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts @@ -14,7 +14,8 @@ const usePatientHtsEncounters = (patientUuid: string) => { 'custom:(uuid,encounterDatetime,location:(uuid,name),' + 'encounterProviders:(uuid,provider:(uuid,name)),' + 'obs:(uuid,obsDatetime,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name))))'; - const antenatalEncounterType = config.encounterTypes.antenatalEncounterType; + const antenatalEncounterType = + config.encounterTypes.antenatalEncounterType || '677d1a80-dbbe-4399-be34-aa7f54f11405'; if (!antenatalEncounterType) { setIsError(true); @@ -51,9 +52,10 @@ const usePatientHivStatus = (patientUuid: string) => { useEffect(() => { const fetchHivStatus = async () => { - const hivTestResultConceptUUID = config.obsConcepts.hivTestResultConceptUUID; - const positiveUUID = config.obsConcepts.positiveUUID; - const negativeUUID = config.obsConcepts.negativeUUID; + const hivTestResultConceptUUID = + config.obsConcepts.hivTestResultConceptUUID || '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const positiveUUID = config.obsConcepts.positiveUUID || '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + const negativeUUID = config.obsConcepts.negativeUUID || '664AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; let hivStatus = ''; diff --git a/packages/esm-ohri-pmtct-app/src/config-schema.ts b/packages/esm-ohri-pmtct-app/src/config-schema.ts index 136b6775c..044731508 100644 --- a/packages/esm-ohri-pmtct-app/src/config-schema.ts +++ b/packages/esm-ohri-pmtct-app/src/config-schema.ts @@ -81,6 +81,9 @@ export const configSchema = { outcomeStatus: '160433AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', infantVisitDate: '159599AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', finalTestResults: '164460AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + hivTestResultConceptUUID: '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + positiveUUID: '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + negativeUUID: '664AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', visitDateConcept: '163260AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', hivTestResultConcept: '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', artNoConcept: '164402AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', diff --git a/packages/esm-ohri-pmtct-app/src/index.ts b/packages/esm-ohri-pmtct-app/src/index.ts index c00bd64e0..e021d3b47 100644 --- a/packages/esm-ohri-pmtct-app/src/index.ts +++ b/packages/esm-ohri-pmtct-app/src/index.ts @@ -14,6 +14,7 @@ import { createNewOHRIDashboardLink, OHRIHome, createConditionalDashboardGroup, + PatientStatusBannerTag, } from '@ohri/openmrs-esm-ohri-commons-lib'; import { configSchema } from './config-schema'; import rootComponent from './root.component'; @@ -98,3 +99,5 @@ export const maternalChildDashboard = getSyncLifecycle(OHRIHome, { }); export const ptrackerReportNavLink = getSyncLifecycle(ptrackerdashboardPath, options); + +export const patientStatusBannerTagExtension = getSyncLifecycle(PatientStatusBannerTag, options); diff --git a/packages/esm-ohri-pmtct-app/src/routes.json b/packages/esm-ohri-pmtct-app/src/routes.json index 9d14b18e1..1db566250 100644 --- a/packages/esm-ohri-pmtct-app/src/routes.json +++ b/packages/esm-ohri-pmtct-app/src/routes.json @@ -5,6 +5,11 @@ }, "pages": [], "extensions": [ + { + "name": "patient-status-banner-tag", + "slot": "patient-banner-tags-slot", + "component": "patientStatusBannerTagExtension" + }, { "name": "maternal-child-health-results-summary", "slot": "homepage-dashboard-slot",