Skip to content

Commit

Permalink
(feat) O3-3901 Add support for program states in esm-patient-programs (
Browse files Browse the repository at this point in the history
…openmrs#1989)

* (feat) O3-3901 Add support for program states in esm-patient-programs

* (refactor) PR comments

* (fix) Failing unit test

* Minor tweaks

---------

Co-authored-by: Dennis Kigen <[email protected]>
  • Loading branch information
ynurmahomed and denniskigen authored Sep 10, 2024
1 parent 2f7ab2f commit 05dc488
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 55 deletions.
8 changes: 8 additions & 0 deletions __mocks__/programs.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const mockEnrolledProgramsResponse = [
{
uuid: '8ba6c08f-66d9-4a18-a233-5f658b1755bf',
program: {
display: 'Human immunodeficiency virus (HIV) disease',
uuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f',
name: 'HIV Care and Treatment',
allWorkflows: [],
Expand All @@ -62,6 +63,7 @@ export const mockEnrolledProgramsResponse = [
},
dateEnrolled: '2020-01-16T00:00:00.000+0000',
dateCompleted: null,
states: [],
},
];

Expand All @@ -84,6 +86,7 @@ export const mockEnrolledInAllProgramsResponse = [
},
dateEnrolled: '2020-01-16T00:00:00.000+0000',
dateCompleted: null,
states: [],
},
{
uuid: '700b7914-9dc9-4569-8fe3-6db6c80af4c5',
Expand All @@ -99,6 +102,7 @@ export const mockEnrolledInAllProgramsResponse = [
},
dateEnrolled: '2021-03-16T00:00:00.000+0000',
dateCompleted: null,
states: [],
},
{
uuid: '874e5326-faa0-4d4b-a891-9a0e3a16f30f',
Expand All @@ -114,11 +118,13 @@ export const mockEnrolledInAllProgramsResponse = [
},
dateEnrolled: '2021-02-16T00:00:00.000+0000',
dateCompleted: null,
states: [],
},
];

export const mockCareProgramsResponse = [
{
name: 'HIV TREATMENT',
uuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f',
display: 'HIV Care and Treatment',
allWorkflows: [],
Expand All @@ -128,6 +134,7 @@ export const mockCareProgramsResponse = [
},
},
{
name: 'ONCOLOGY SCREENING AND DIAGNOSIS PROGRAM',
uuid: '11b129ca-a5e7-4025-84bf-b92a173e20de',
display: 'Oncology Screening and Diagnosis',
allWorkflows: [],
Expand All @@ -137,6 +144,7 @@ export const mockCareProgramsResponse = [
},
},
{
name: 'HIV DIFFERENTIATED CARE PROGRAM',
uuid: 'b2f65a51-2f87-4faa-a8c6-327a0c1d2e17',
display: 'HIV Differentiated Care',
allWorkflows: [],
Expand Down
7 changes: 7 additions & 0 deletions packages/esm-patient-programs-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ export const configSchema = {
_type: Type.Boolean,
_default: false,
},
showProgramStatusField: {
_type: Type.Boolean,
_description:
'Whether to show the Program status field in the Record program enrollment and Edit program enrollment forms. If set to true, the `Program status` field is displayed in the Programs datatable',
_default: false,
},
};

export interface ConfigObject {
hideAddProgramButton: boolean;
showProgramStatusField: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
isDesktop as desktopLayout,
} from '@openmrs/esm-framework';
import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
import { usePrograms } from './programs.resource';
import { findLastState, usePrograms } from './programs.resource';
import styles from './programs-detailed-summary.scss';

interface ProgramsDetailedSummaryProps {
Expand All @@ -40,7 +40,7 @@ interface ProgramEditButtonProps {

const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patientUuid }) => {
const { t } = useTranslation();
const { hideAddProgramButton } = useConfig<ConfigObject>();
const { hideAddProgramButton, showProgramStatusField } = useConfig<ConfigObject>();
const layout = useLayoutType();
const isTablet = layout === 'tablet';
const isDesktop = desktopLayout(layout);
Expand All @@ -49,8 +49,8 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patie

const { enrollments, isLoading, error, isValidating, availablePrograms } = usePrograms(patientUuid);

const tableHeaders: Array<typeof DataTableHeader> = useMemo(
() => [
const tableHeaders: Array<typeof DataTableHeader> = useMemo(() => {
const headers = [
{
key: 'display',
header: t('activePrograms', 'Active programs'),
Expand All @@ -67,21 +67,31 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patie
key: 'status',
header: t('status', 'Status'),
},
],
[t],
);
];
if (showProgramStatusField) {
headers.push({
key: 'state',
header: t('programStatus', 'Program status'),
});
}
return headers;
}, [t, showProgramStatusField]);

const tableRows = useMemo(
() =>
enrollments?.map((program) => ({
id: program.uuid,
display: program.display,
location: program.location?.display ?? '--',
dateEnrolled: formatDatetime(new Date(program.dateEnrolled)),
status: program.dateCompleted
? `${t('completedOn', 'Completed On')} ${formatDate(new Date(program.dateCompleted))}`
: t('active', 'Active'),
})),
enrollments?.map((program) => {
const state = program ? findLastState(program.states) : null;
return {
id: program.uuid,
display: program.display,
location: program.location?.display ?? '--',
dateEnrolled: formatDatetime(new Date(program.dateEnrolled)),
status: program.dateCompleted
? `${t('completedOn', 'Completed On')} ${formatDate(new Date(program.dateCompleted))}`
: t('active', 'Active'),
state: state ? state.state.concept.display : '--',
};
}),
[enrollments, t],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { screen, within } from '@testing-library/react';
import { openmrsFetch } from '@openmrs/esm-framework';
import { getDefaultsFromConfigSchema, openmrsFetch, useConfig } from '@openmrs/esm-framework';
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
import { mockCareProgramsResponse, mockEnrolledInAllProgramsResponse, mockEnrolledProgramsResponse } from '__mocks__';
import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
import { type ConfigObject, configSchema } from '../config-schema';
import ProgramsDetailedSummary from './programs-detailed-summary.component';

const mockOpenmrsFetch = openmrsFetch as jest.Mock;
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);

jest.mock('@openmrs/esm-patient-common-lib', () => {
const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib');
Expand Down Expand Up @@ -107,4 +109,21 @@ describe('ProgramsDetailedSummary', () => {
expect(screen.getByText(/enrolled in all programs/i)).toBeInTheDocument();
expect(screen.getByText(/there are no more programs left to enroll this patient in/i)).toBeInTheDocument();
});

it('renders the programs status field', async () => {
const user = userEvent.setup();

mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } });

mockUseConfig.mockReturnValue({
...getDefaultsFromConfigSchema(configSchema),
showProgramStatusField: true,
});

renderWithSwr(<ProgramsDetailedSummary patientUuid={mockPatient.id} />);

await waitForLoadingToFinish();

expect(screen.getByRole('columnheader', { name: /program status/i })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { type FetchResponse, showSnackbar, useLocations } from '@openmrs/esm-framework';
import {
type FetchResponse,
showSnackbar,
useLocations,
useConfig,
getDefaultsFromConfigSchema,
} from '@openmrs/esm-framework';
import { mockCareProgramsResponse, mockEnrolledProgramsResponse, mockLocationsResponse } from '__mocks__';
import { mockPatient } from 'tools';
import {
Expand All @@ -11,6 +17,7 @@ import {
useEnrollments,
} from './programs.resource';
import ProgramsForm from './programs-form.workspace';
import { type ConfigObject, configSchema } from '../config-schema';

const mockUseAvailablePrograms = jest.mocked(useAvailablePrograms);
const mockUseEnrollments = jest.mocked(useEnrollments);
Expand All @@ -21,6 +28,7 @@ const mockUseLocations = jest.mocked(useLocations);
const mockCloseWorkspace = jest.fn();
const mockCloseWorkspaceWithSavedChanges = jest.fn();
const mockPromptBeforeClosing = jest.fn();
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);

const testProps = {
closeWorkspace: mockCloseWorkspace,
Expand All @@ -38,6 +46,7 @@ jest.mock('./programs.resource', () => ({
updateProgramEnrollment: jest.fn(),
useAvailablePrograms: jest.fn(),
useEnrollments: jest.fn(),
findLastState: jest.fn(),
}));

mockUseLocations.mockReturnValue(mockLocationsResponse);
Expand Down Expand Up @@ -145,6 +154,17 @@ describe('ProgramsForm', () => {
}),
);
});

it('renders the programs status field if the config property is set to true', async () => {
mockUseConfig.mockReturnValue({
...getDefaultsFromConfigSchema(configSchema),
showProgramStatusField: true,
});

renderProgramsForm();

expect(screen.getByLabelText(/program status/i)).toBeInTheDocument();
});
});

function renderProgramsForm(programEnrollmentUuidToEdit?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import {
Stack,
} from '@carbon/react';
import { z } from 'zod';
import { useForm, Controller } from 'react-hook-form';
import { useForm, Controller, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { parseDate, showSnackbar, useLayoutType, useLocations, useSession } from '@openmrs/esm-framework';
import { parseDate, showSnackbar, useConfig, useLayoutType, useLocations, useSession } from '@openmrs/esm-framework';
import { type DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib';
import {
createProgramEnrollment,
useAvailablePrograms,
useEnrollments,
updateProgramEnrollment,
findLastState,
} from './programs.resource';
import styles from './programs-form.scss';

Expand All @@ -38,6 +39,7 @@ const createProgramsFormSchema = (t: TFunction) =>
enrollmentDate: z.date(),
completionDate: z.date().nullable(),
enrollmentLocation: z.string(),
selectedProgramStatus: z.string(),
});

export type ProgramsFormData = z.infer<ReturnType<typeof createProgramsFormSchema>>;
Expand All @@ -56,6 +58,7 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({
const { data: availablePrograms } = useAvailablePrograms();
const { data: enrollments, mutateEnrollments } = useEnrollments(patientUuid);
const [isSubmittingForm, setIsSubmittingForm] = useState(false);
const { showProgramStatusField } = useConfig();

const programsFormSchema = useMemo(() => createProgramsFormSchema(t), [t]);

Expand All @@ -81,6 +84,8 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({
return currentEnrollment?.location.uuid ?? null;
};

const currentState = currentEnrollment ? findLastState(currentEnrollment.states) : null;

const {
control,
handleSubmit,
Expand All @@ -94,26 +99,35 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({
enrollmentDate: currentEnrollment?.dateEnrolled ? parseDate(currentEnrollment.dateEnrolled) : new Date(),
completionDate: currentEnrollment?.dateCompleted ? parseDate(currentEnrollment.dateCompleted) : null,
enrollmentLocation: getLocationUuid() ?? '',
selectedProgramStatus: currentState?.state.uuid ?? '',
},
});

const selectedProgram = useWatch({ control, name: 'selectedProgram' });

useEffect(() => {
promptBeforeClosing(() => isDirty);
}, [isDirty, promptBeforeClosing]);

const onSubmit = useCallback(
async (data: ProgramsFormData) => {
const { selectedProgram, enrollmentDate, completionDate, enrollmentLocation } = data;
const { selectedProgram, enrollmentDate, completionDate, enrollmentLocation, selectedProgramStatus } = data;

const payload = {
patient: patientUuid,
program: selectedProgram,
dateEnrolled: enrollmentDate ? dayjs(enrollmentDate).format() : null,
dateCompleted: completionDate ? dayjs(completionDate).format() : null,
location: enrollmentLocation,
states:
!!selectedProgramStatus && selectedProgramStatus != currentState?.state.uuid
? [{ state: { uuid: selectedProgramStatus } }]
: [],
};

try {
setIsSubmittingForm(true);

const abortController = new AbortController();

if (currentEnrollment) {
Expand Down Expand Up @@ -144,7 +158,7 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({

setIsSubmittingForm(false);
},
[closeWorkspaceWithSavedChanges, currentEnrollment, mutateEnrollments, patientUuid, t],
[closeWorkspaceWithSavedChanges, currentEnrollment, currentState, mutateEnrollments, patientUuid, t],
);

const programSelect = (
Expand Down Expand Up @@ -241,6 +255,41 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({
/>
);

let workflowStates = [];
if (!currentProgram && !!selectedProgram) {
const program = eligiblePrograms.find((p) => p.uuid === selectedProgram);
if (program?.allWorkflows.length > 0) workflowStates = program.allWorkflows[0].states;
} else if (currentProgram?.allWorkflows.length > 0) {
workflowStates = currentProgram.allWorkflows[0].states;
}

const programStatusDropdown = (
<Controller
name="selectedProgramStatus"
control={control}
render={({ fieldState, field: { onChange, value } }) => (
<>
<Select
aria-label={t('programStatus', 'Program status')}
id="programStatus"
invalid={!!fieldState?.error}
labelText={t('programStatus', 'Program status')}
onChange={(event) => onChange(event.target.value)}
value={value}
>
<SelectItem text={t('chooseStatus', 'Choose a program status')} value="" />
{workflowStates.map((state) => (
<SelectItem key={state.uuid} text={state.concept.display} value={state.uuid}>
{state.concept.display}
</SelectItem>
))}
</Select>
<p className={styles.errorMessage}>{fieldState?.error?.message}</p>
</>
)}
/>
);

const formGroups = [
{
style: { maxWidth: isTablet && '50%' },
Expand All @@ -264,6 +313,14 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({
},
];

if (showProgramStatusField) {
formGroups.push({
style: { width: '50%' },
legendText: '',
value: programStatusDropdown,
});
}

return (
<Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<Stack className={styles.formContainer} gap={7}>
Expand Down
Loading

0 comments on commit 05dc488

Please sign in to comment.