Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OHRI-2280 Latest HIV Test Result displayed on patient banner #1914

Merged
merged 2 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading