From fada1f2c8368829820c46e10f7be72bb7b8068cd Mon Sep 17 00:00:00 2001
From: AJAL ODORA JONATHAN <43242517+ODORA0@users.noreply.github.com>
Date: Wed, 31 Jul 2024 12:58:15 +0300
Subject: [PATCH] OHRI-2280 Latest HIV Test Result displayed on patient banner
(#1914)
* OHRI-2280 Latest HIV Test Result displayed on patient banner
* Add configs to PMTCT
---
.../banner-tags/.patient-status-tag.test.tsx | 83 +++++++++--
.../patient-status-tag.component.tsx | 20 +--
.../banner-tags/patientHivStatus.ts | 132 ++++++++++++++----
.../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 +-
.../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 +
10 files changed, 277 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..87b17715a 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,118 @@
-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 || '677d1a80-dbbe-4399-be34-aa7f54f11405';
+
+ 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 || '159427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
+ const positiveUUID = config.obsConcepts.positiveUUID || '138571AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
+ const negativeUUID = config.obsConcepts.negativeUUID || '664AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
- 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": [
{
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",