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

(feat) O3-3316 Add support for encounter diagnosis #298

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
80 changes: 80 additions & 0 deletions src/adapters/encounter-diagnoses-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { type OpenmrsResource } from '@openmrs/esm-framework';
import { type FormFieldValueAdapter, type FormProcessorContextProps } from '..';
import { type FormContextProps } from '../provider/form-provider';
import { type OpenmrsEncounter, type FormField } from '../types';
import { clearSubmission, gracefullySetSubmission } from '../utils/common-utils';

export let assignedDiagnosesIds: string[] = [];

export const EncounterDiagnosesAdapter: FormFieldValueAdapter = {
transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
if (context.sessionMode == 'edit' && field.meta?.previousValue?.uuid) {
return editDiagnosis(value, field);
}
const newValue = constructNewDiagnosis(value, field, context.patient.id);
gracefullySetSubmission(field, newValue, null);
return newValue;
},
getInitialValue: function (
field: FormField,
sourceObject: OpenmrsResource,
context: FormProcessorContextProps,
): Promise<any> {
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter);
const encounter = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter);

const matchedDiagnoses = availableDiagnoses.diagnoses.find(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const matchedDiagnoses = availableDiagnoses.diagnoses.find(
const matchedDiagnosis = availableDiagnoses.diagnoses.find(

(diagnosis) => diagnosis.formFieldPath === `rfe-forms-${field.id}`,
);

if (matchedDiagnoses) {
field.meta = { ...(field.meta || {}), previousValue: matchedDiagnoses };
if (!assignedDiagnosesIds.includes(matchedDiagnoses.diagnosis?.coded?.uuid)) {
assignedDiagnosesIds.push(matchedDiagnoses.diagnosis?.coded?.uuid);
}
return matchedDiagnoses.diagnosis?.coded.uuid;
}
return null;
},
getPreviousValue: function (
field: FormField,
sourceObject: OpenmrsResource,
context: FormProcessorContextProps,
): Promise<any> {
return null;
},
getDisplayValue: (field: FormField, value: any) => {
return field.questionOptions.answers?.find((option) => option.concept == value)?.label || value;
},
tearDown: function (): void {
assignedDiagnosesIds = [];
},
};

const constructNewDiagnosis = (value: any, field: FormField, patientUuid: string) => {
if (!value) {
return null;
}
return {
patient: patientUuid,
condition: null,
diagnosis: {
coded: value,
},
certainty: 'CONFIRMED',
rank: field.questionOptions.rank, // rank 1 denotes a diagnosis is primary, else secondary
formFieldPath: `rfe-forms-${field.id}`,
formFieldNamespace: 'rfe-forms',
};
};

function editDiagnosis(newEncounterDiagnosis: any, field: FormField) {
if (newEncounterDiagnosis === field.meta.previousValue?.concept?.uuid) {
clearSubmission(field);
return null;
}
const voided = {
uuid: field.meta.previousValue?.uuid,
voided: true,
};
gracefullySetSubmission(field, constructNewDiagnosis(newEncounterDiagnosis, field, null), voided);
return field.meta.submission.newValue || null;
}
166 changes: 166 additions & 0 deletions src/adapters/encounter-diagnoses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { type FormContextProps } from '../provider/form-provider';
import { type FormField } from '../types';
import { EncounterDiagnosesAdapter } from './encounter-diagnoses-adapter';

const formContext = {
methods: null,
workspaceLayout: 'maximized',
isSubmitting: false,
patient: {
id: '833db896-c1f0-11eb-8529-0242ac130003',
},
formJson: null,
visit: null,
sessionMode: 'enter',
sessionDate: new Date(),
location: {
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
},
currentProvider: null,
layoutType: 'small-desktop',
domainObjectValue: {
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
obs: [],
diagnoses: [],
},
previousDomainObjectValue: null,
processor: null,
formFields: [],
formFieldAdapters: null,
formFieldValidators: null,
customDependencies: {
patientPrograms: [],
},
getFormField: jest.fn(),
addFormField: jest.fn(),
updateFormField: jest.fn(),
removeFormField: () => {},
addInvalidField: jest.fn(),
removeInvalidField: jest.fn(),
setInvalidFields: jest.fn(),
setForm: jest.fn(),
} as FormContextProps;

const field = {
label: 'Test Diagnosis',
id: 'DiagNosIS',
type: 'diagnosis',
questionOptions: {
rendering: 'repeating',
rank: 1,
datasource: {
name: 'problem_datasource',
config: {
class: [
'8d4918b0-c2cc-11de-8d13-0010c6dffd0f',
'8d492954-c2cc-11de-8d13-0010c6dffd0f',
'8d492b2a-c2cc-11de-8d13-0010c6dffd0f',
],
},
},
},
meta: {
submission: {
newValue: null,
},
},
validators: [
{
type: 'form_field',
},
{
type: 'default_value',
},
],
isHidden: false,
isRequired: false,
isDisabled: false,
} as FormField;

const diagnoses = [
{
uuid: '8d975f9e-e9e6-452f-be7c-0e87c047f056',
diagnosis: {
coded: {
uuid: '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
display: 'Schistosoma Mansonii Infection',
links: [],
},
},
condition: null,
encounter: {
uuid: '9a4b06bd-d655-414f-b9ce-69e940c337ce',
},
certainty: 'CONFIRMED',
rank: 1,
voided: false,
display: 'Schistosoma Mansonii Infection',
patient: {
uuid: '00affa97-0010-417c-87f5-de48362de915',
display: '1000VKV - Bett Tett',
},
formFieldNamespace: 'rfe-forms',
formFieldPath: 'rfe-forms-DiagNosIS_1',
links: [],
resourceVersion: '1.8',
},
{
uuid: 'b2d0e95b-d2f6-49d1-a477-acc7026edbd7',
diagnosis: {
coded: {
uuid: '137329AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
display: 'Infection due to Entamoeba Histolytica',
links: [],
},
},
condition: null,
encounter: {
uuid: '9a4b06bd-d655-414f-b9ce-69e940c337ce',
},
certainty: 'CONFIRMED',
rank: 1,
voided: false,
display: 'Infection due to Entamoeba Histolytica',
patient: {
uuid: '00affa97-0010-417c-87f5-de48362de915',
display: '1000VKV - Bett Tett',
},
formFieldNamespace: 'rfe-forms',
formFieldPath: 'rfe-forms-DiagNosIS',
links: [],
resourceVersion: '1.8',
},
];

describe('EncounterDiagnosesAdapter', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add test case(s) around:

  • editing a value and the fact that the edited value gets voided

it('should should handle submission of a diagnosis field', async () => {
const value = '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
EncounterDiagnosesAdapter.transformFieldValue(field, value, formContext);
expect(field.meta.submission.newValue).toEqual({
patient: '833db896-c1f0-11eb-8529-0242ac130003',
condition: null,
diagnosis: {
coded: '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
},
certainty: 'CONFIRMED',
rank: 1,
formFieldPath: 'rfe-forms-DiagNosIS',
formFieldNamespace: 'rfe-forms',
});
});

it('should get initial value for the diagnosis', async () => {
formContext.domainObjectValue.diagnoses.push(...diagnoses);
const program = await EncounterDiagnosesAdapter.getInitialValue(field, null, formContext);
expect(program).toEqual('137329AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
});

it('should return null for getPreviousValue', async () => {
const previousValue = await EncounterDiagnosesAdapter.getPreviousValue(field, null, formContext);
expect(previousValue).toBeNull();
});

it('should execute tearDown without issues', () => {
expect(() => EncounterDiagnosesAdapter.tearDown()).not.toThrow();
});
});
1 change: 1 addition & 0 deletions src/adapters/orders-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const OrdersAdapter: FormFieldValueAdapter = {
}
const newValue = constructNewOrder(value, field, context.currentProvider.uuid);
gracefullySetSubmission(field, newValue, null);

return newValue;
},
getInitialValue: function (
Expand Down
1 change: 1 addition & 0 deletions src/components/repeat/repeat.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useFormFactory } from '../../provider/form-factory-provider';
const renderingByTypeMap: Record<string, RenderType> = {
obsGroup: 'group',
testOrder: 'select',
diagnosis: 'ui-select-extended',
};

const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const encounterRepresentation =
'custom:(uuid,encounterDatetime,encounterType:(uuid,name,description),location:(uuid,name),' +
'patient:(uuid,display),encounterProviders:(uuid,provider:(uuid,name),encounterRole:(uuid,name)),' +
'orders:(uuid,display,concept:(uuid,display),voided),' +
'diagnoses:(uuid,certainty,condition,formFieldPath,formFieldNamespace,display,rank,voided,diagnosis:(coded:(uuid,display))),' +
'obs:(uuid,obsDatetime,comment,voided,groupMembers,formFieldNamespace,formFieldPath,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name),' +
'names:(uuid,conceptNameType,name))))';
export const FormsStore = 'forms-engine-store';
Expand Down
10 changes: 10 additions & 0 deletions src/processors/encounter/encounter-form-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export class EncounterFormProcessor extends FormProcessor {
try {
const { data: savedEncounter } = await saveEncounter(abortController, encounter, encounter.uuid);
const saveOrders = savedEncounter.orders.map((order) => order.orderNumber);
const saveDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const saveDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display);
const savedDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display);

(I would also rename saveOrders -> savedOrders)

if (saveOrders.length) {
showSnackbar({
title: translateFn('ordersSaved', 'Order(s) saved successfully'),
Expand All @@ -174,6 +175,15 @@ export class EncounterFormProcessor extends FormProcessor {
isLowContrast: true,
});
}
// handle diagnoses
if (saveDiagnoses.length) {
showSnackbar({
title: translateFn('diagnosisSaved', 'Diagnosis(s) saved successfully'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: translateFn('diagnosisSaved', 'Diagnosis(s) saved successfully'),
title: translateFn('diagnosisSaved', 'Diagnosis(es) saved successfully'),

subtitle: saveDiagnoses.join(', '),
kind: 'success',
isLowContrast: true,
});
}
// handle attachments
try {
const attachmentsResponse = await Promise.all(
Expand Down
44 changes: 44 additions & 0 deletions src/processors/encounter/encounter-processor-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ConceptTrue } from '../../constants';
import { DefaultValueValidator } from '../../validators/default-value-validator';
import { cloneRepeatField } from '../../components/repeat/helpers';
import { assignedOrderIds } from '../../adapters/orders-adapter';
import { assignedDiagnosesIds } from '../../adapters/encounter-diagnoses-adapter';

export function prepareEncounter(
context: FormContextProps,
Expand All @@ -28,6 +29,7 @@ export function prepareEncounter(
const obsForSubmission = [];
prepareObs(obsForSubmission, formFields);
const ordersForSubmission = prepareOrders(formFields);
const diagnosesForSubmission = prepareDiagnosis(formFields);
let encounterForSubmission: OpenmrsEncounter = {};

if (encounter) {
Expand Down Expand Up @@ -57,6 +59,7 @@ export function prepareEncounter(
}
encounterForSubmission.obs = obsForSubmission;
encounterForSubmission.orders = ordersForSubmission;
encounterForSubmission.diagnoses = diagnosesForSubmission;
} else {
encounterForSubmission = {
patient: patient.id,
Expand All @@ -75,6 +78,7 @@ export function prepareEncounter(
},
visit: visit?.uuid,
orders: ordersForSubmission,
diagnoses: diagnosesForSubmission,
};
}
return encounterForSubmission;
Expand Down Expand Up @@ -300,6 +304,40 @@ export async function hydrateRepeatField(
}),
);
}

const unMappedDiagnoses = encounter.diagnoses.filter((diagnosis) => {
return !assignedDiagnosesIds.includes(diagnosis?.diagnosis?.coded.uuid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Diagnosis should include the field's ID in it's formFieldPath otherwise we may end up including past diagnoses.

});

const sortedDiagnoses = unMappedDiagnoses
.filter((diagnosis) => !diagnosis.voided)
.sort((a, b) => {
// Extract numeric part of formFieldPath for sorting
const numberA = parseInt(a.formFieldPath.split('_')[1], 10);
const numberB = parseInt(b.formFieldPath.split('_')[1], 10);
return numberA - numberB; // Sort numerically based on formFieldPath
});

if (field.type === 'diagnosis') {
return Promise.all(
sortedDiagnoses
.filter((diagnosis) => !diagnosis.voided)
.map(async (diagnosis) => {
const clone = cloneRepeatField(field, diagnosis, counter++);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a demo where we capture diagnoses with "repeat controls"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

initialValues[clone.id] = await formFieldAdapters[field.type].getInitialValue(
clone,
{ diagnoses: [diagnosis] } as any,
context,
);

if (!assignedDiagnosesIds.includes(diagnosis.diagnosis.coded.uuid)) {
assignedDiagnosesIds.push(diagnosis.diagnosis.coded.uuid);
}

return clone;
}),
);
}
// handle obs groups
return Promise.all(
unMappedGroups.map(async (group) => {
Expand All @@ -318,3 +356,9 @@ export async function hydrateRepeatField(
}),
).then((results) => results.flat());
}

function prepareDiagnosis(fields: FormField[]) {
return fields
.filter((field) => field.type === 'diagnosis' && hasSubmission(field))
.map((field) => field.meta.submission.newValue);
}
5 changes: 5 additions & 0 deletions src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ObsCommentAdapter } from '../../adapters/obs-comment-adapter';
import { OrdersAdapter } from '../../adapters/orders-adapter';
import { PatientIdentifierAdapter } from '../../adapters/patient-identifier-adapter';
import { ProgramStateAdapter } from '../../adapters/program-state-adapter';
import { EncounterDiagnosesAdapter } from '../../adapters/encounter-diagnoses-adapter';
import { type FormFieldValueAdapter } from '../../types';

export const inbuiltFieldValueAdapters: RegistryItem<FormFieldValueAdapter>[] = [
Expand Down Expand Up @@ -61,4 +62,8 @@ export const inbuiltFieldValueAdapters: RegistryItem<FormFieldValueAdapter>[] =
type: 'patientIdentifier',
component: PatientIdentifierAdapter,
},
{
type: 'diagnosis',
component: EncounterDiagnosesAdapter,
},
];
Loading