Skip to content

Commit

Permalink
OHRI-2280 Latest HIV Test Result displayed on patient banner (#1914)
Browse files Browse the repository at this point in the history
* OHRI-2280 Latest HIV Test Result displayed on patient banner

* Add configs to PMTCT
  • Loading branch information
ODORA0 authored Jul 31, 2024
1 parent b7ea83a commit fada1f2
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});

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(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});
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(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});

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(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});

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(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});

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(<PatientStatusBannerTag patientUuid="sampleUuid" />);
render(<PatientStatusBannerTag patientUuid={hivPositiveSampleUuid} />);
});

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
});
});
Original file line number Diff line number Diff line change
@@ -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 && <Tag type="red">{t('hivPositive', 'HIV Positive')}</Tag>}</>;
return (
<>
{hivStatus === 'positive' && <Tag type="red">{t('hivPositive', 'HIV Positive')}</Tag>}
{hivStatus === 'negative' && <Tag type="green">{t('hivNegative', 'HIV Negative')}</Tag>}
</>
);
}

export default PatientStatusBannerTag;
132 changes: 103 additions & 29 deletions packages/esm-commons-lib/src/components/banner-tags/patientHivStatus.ts
Original file line number Diff line number Diff line change
@@ -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<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(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<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(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 };
Original file line number Diff line number Diff line change
@@ -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);
});
});
25 changes: 25 additions & 0 deletions packages/esm-commons-lib/src/config.schema.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 11 additions & 1 deletion packages/esm-commons-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Loading

0 comments on commit fada1f2

Please sign in to comment.