diff --git a/__mocks__/forms/rfe-forms/diagnosis-test-form.json b/__mocks__/forms/rfe-forms/diagnosis-test-form.json new file mode 100644 index 000000000..c60ffdb9b --- /dev/null +++ b/__mocks__/forms/rfe-forms/diagnosis-test-form.json @@ -0,0 +1,60 @@ +{ + "name": "Diagnosis test", + "pages": [ + { + "label": "1", + "sections": [ + { + "label": "Select diagnosis", + "isExpanded": "true", + "questions": [ + { + "label": "Test Diagnosis 1", + "id": "diagnosis1", + "type": "diagnosis", + "questionOptions": { + "rendering": "repeating", + "dataSource": "diagnoses", + "isSearchable": "false", + "diagnosis": { + "isConfirmed": "true", + "rank": 1, + "conceptClasses": [ + "8d4918b0-c2cc-11de-8d13-0010c6dffd0f", + "8d492954-c2cc-11de-8d13-0010c6dffd0f", + "8d492b2a-c2cc-11de-8d13-0010c6dffd0f" + ] + } + } + }, + { + "label": "Test Diagnosis 2", + "id": "diagnosis2", + "type": "diagnosis", + "questionOptions": { + "rendering": "repeating", + "dataSource": "diagnoses", + "isSearchable": "false", + "diagnosis": { + "isConfirmed": "true", + "rank": 1, + "conceptClasses": [ + "8d4918b0-c2cc-11de-8d13-0010c6dffd0f", + "8d492954-c2cc-11de-8d13-0010c6dffd0f", + "8d492b2a-c2cc-11de-8d13-0010c6dffd0f" + ] + } + } + } + ] + } + ] + } + ], + "processor": "EncounterFormProcessor", + "encounterType": "e22e39fd-7db2-45e7-80f1-60fa0d5a4378", + "referencedForms": [], + "uuid": "29ce67d1-892b-45be-84ce-8e36aa9ca37f", + "description": "re", + "version": "1.0" +} diff --git a/src/adapters/encounter-diagnosis-adapter.test.ts b/src/adapters/encounter-diagnosis-adapter.test.ts new file mode 100644 index 000000000..e13241731 --- /dev/null +++ b/src/adapters/encounter-diagnosis-adapter.test.ts @@ -0,0 +1,232 @@ +import { type FormContextProps } from '../provider/form-provider'; +import { type FormField } from '../types'; +import { EncounterDiagnosisAdapter } from './encounter-diagnosis-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: [], + }, + deletedFields: [], + getFormField: jest.fn(), + addFormField: jest.fn(), + updateFormField: jest.fn(), + removeFormField: () => {}, + addInvalidField: jest.fn(), + removeInvalidField: jest.fn(), + setInvalidFields: jest.fn(), + setForm: jest.fn(), + setDeletedFields: jest.fn(), +} as FormContextProps; + +const field = { + label: 'Test Diagnosis', + id: 'DiagNosIS', + type: 'diagnosis', + questionOptions: { + rendering: 'repeating', + diagnosis: { + rank: 1, + isConfirmed: false, + }, + 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, + }, + previousValue: 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: 'PROVISIONAL', + 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('EncounterDiagnosisAdapter', () => { + it('should should handle submission of a diagnosis field', async () => { + const value = '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + EncounterDiagnosisAdapter.transformFieldValue(field, value, formContext); + expect(field.meta.submission.newValue).toEqual({ + patient: '833db896-c1f0-11eb-8529-0242ac130003', + condition: null, + diagnosis: { + coded: '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + certainty: 'PROVISIONAL', + rank: 1, + formFieldPath: 'rfe-forms-DiagNosIS', + formFieldNamespace: 'rfe-forms', + }); + }); + + it('should get initial value for the diagnosis', async () => { + formContext.domainObjectValue.diagnoses.push(...diagnoses); + const diagnosis = await EncounterDiagnosisAdapter.getInitialValue(field, null, formContext); + expect(diagnosis).toEqual('137329AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); + }); + + it('should return null for getPreviousValue', async () => { + const previousValue = await EncounterDiagnosisAdapter.getPreviousValue(field, null, formContext); + expect(previousValue).toBeNull(); + }); + + it('should execute tearDown without issues', () => { + expect(() => EncounterDiagnosisAdapter.tearDown()).not.toThrow(); + }); + + it('should edit a diagnosis value', () => { + formContext.sessionMode = 'edit'; + + const value = '128138AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + field.meta = { + initialValue: { + omrsObject: { + uuid: '0e20bb67-5d7f-41e0-96a1-751efc21a96f', + diagnosis: { + coded: { + uuid: '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Schistosoma Mansonii Infection', + }, + }, + }, + refinedValue: null, + }, + }; + + EncounterDiagnosisAdapter.transformFieldValue(field, value, formContext); + expect(field.meta.submission.newValue).toEqual({ + patient: '833db896-c1f0-11eb-8529-0242ac130003', + condition: null, + diagnosis: { + coded: '128138AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + certainty: 'PROVISIONAL', + rank: 1, + formFieldPath: 'rfe-forms-DiagNosIS', + formFieldNamespace: 'rfe-forms', + uuid: '0e20bb67-5d7f-41e0-96a1-751efc21a96f', + }); + expect(field.meta.submission.voidedValue).toBe(undefined); + }); + + it('should void removed diagnosis in edit mode', () => { + formContext.sessionMode = 'edit'; + + field.meta = { + initialValue: { + omrsObject: { + uuid: '0e20bb67-5d7f-41e0-96a1-751efc21a96f', + diagnosis: { + coded: { + uuid: '127133AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Schistosoma Mansonii Infection', + }, + }, + }, + refinedValue: null, + }, + }; + + EncounterDiagnosisAdapter.transformFieldValue(field, null, formContext); + expect(field.meta.submission.voidedValue).toEqual({ + voided: true, + uuid: '0e20bb67-5d7f-41e0-96a1-751efc21a96f', + }); + }); +}); diff --git a/src/adapters/encounter-diagnosis-adapter.ts b/src/adapters/encounter-diagnosis-adapter.ts new file mode 100644 index 000000000..b218bb0bb --- /dev/null +++ b/src/adapters/encounter-diagnosis-adapter.ts @@ -0,0 +1,116 @@ +import { type OpenmrsResource } from '@openmrs/esm-framework'; +import { type OpenmrsObs, type FormFieldValueAdapter, type FormProcessorContextProps } from '../types'; +import { type FormContextProps } from '../provider/form-provider'; +import { type OpenmrsEncounter, type FormField } from '../types'; +import { gracefullySetSubmission } from '../utils/common-utils'; +import { isEmpty } from '../validators/form-validator'; +import { isTrue } from '../utils/boolean-utils'; + +export let assignedDiagnosesIds: string[] = []; + +export const EncounterDiagnosisAdapter: FormFieldValueAdapter = { + transformFieldValue: function (field: FormField, value: any, context: FormContextProps) { + if (field.meta.initialValue?.omrsObject && isEmpty(value)) { + return gracefullySetSubmission(field, undefined, voidDiagnosis(field.meta.initialValue.omrsObject as OpenmrsObs)); + } + if (!isEmpty(value)) { + const previousDiagnosis = field.meta.initialValue?.omrsObject as OpenmrsResource; + if (hasPreviousDiagnosisValueChanged(previousDiagnosis, value)) { + return gracefullySetSubmission( + field, + editDiagnosis(value, field, previousDiagnosis, context.patient.id), + undefined, + ); + } + } + const newValue = constructNewDiagnosis(value, field, context.patient.id); + gracefullySetSubmission(field, newValue, null); + return newValue; + }, + getInitialValue: function ( + field: FormField, + sourceObject: OpenmrsResource, + context: FormProcessorContextProps, + ): Promise { + const encounter = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); + const matchedDiagnosis = encounter.diagnoses.find( + (diagnosis) => diagnosis.formFieldPath === `rfe-forms-${field.id}`, + ); + + if (matchedDiagnosis) { + field.meta = { + ...(field.meta || {}), + initialValue: { + omrsObject: matchedDiagnosis, + refinedValue: matchedDiagnosis.diagnosis?.coded.uuid, + }, + }; + if (!assignedDiagnosesIds.includes(matchedDiagnosis.diagnosis?.coded?.uuid)) { + assignedDiagnosesIds.push(matchedDiagnosis.diagnosis?.coded?.uuid); + } + return matchedDiagnosis.diagnosis?.coded.uuid; + } + return null; + }, + getPreviousValue: function ( + field: FormField, + sourceObject: OpenmrsResource, + context: FormProcessorContextProps, + ): Promise { + 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: string, field: FormField, patientUuid: string) => { + if (!value) { + return null; + } + return { + patient: patientUuid, + condition: null, + diagnosis: { + coded: value, + }, + certainty: isTrue(field.questionOptions?.diagnosis?.isConfirmed) ? 'CONFIRMED' : 'PROVISIONAL', + rank: field.questionOptions.diagnosis?.rank ?? 1, // rank 1 denotes a diagnosis is primary, else secondary + formFieldPath: `rfe-forms-${field.id}`, + formFieldNamespace: 'rfe-forms', + }; +}; + +function editDiagnosis( + newEncounterDiagnosis: string, + field: FormField, + previousDiagnosis: OpenmrsResource, + patientUuid: string, +) { + return { + patient: patientUuid, + condition: null, + diagnosis: { + coded: newEncounterDiagnosis, + }, + certainty: isTrue(field.questionOptions?.diagnosis?.isConfirmed) ? 'CONFIRMED' : 'PROVISIONAL', + rank: field.questionOptions.diagnosis?.rank ?? 1, // rank 1 denotes a diagnosis is primary, else secondary + formFieldPath: `rfe-forms-${field.id}`, + formFieldNamespace: 'rfe-forms', + uuid: previousDiagnosis.uuid, + }; +} + +export function hasPreviousDiagnosisValueChanged(previousDiagnosis: OpenmrsResource, newValue: string) { + if (isEmpty(previousDiagnosis)) { + return false; + } + return previousDiagnosis.value !== newValue; +} + +export function voidDiagnosis(obs: OpenmrsObs) { + return { uuid: obs.uuid, voided: true }; +} diff --git a/src/components/repeat/repeat.component.tsx b/src/components/repeat/repeat.component.tsx index 139e82ec6..887d51541 100644 --- a/src/components/repeat/repeat.component.tsx +++ b/src/components/repeat/repeat.component.tsx @@ -15,6 +15,7 @@ import { useFormFactory } from '../../provider/form-factory-provider'; const renderingByTypeMap: Record = { obsGroup: 'group', testOrder: 'select', + diagnosis: 'ui-select-extended', }; const Repeat: React.FC = ({ field }) => { diff --git a/src/constants.ts b/src/constants.ts index 567569286..88ee7c866 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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'; diff --git a/src/datasources/concept-data-source.ts b/src/datasources/concept-data-source.ts index a70a2fda2..c1655ab4f 100644 --- a/src/datasources/concept-data-source.ts +++ b/src/datasources/concept-data-source.ts @@ -18,11 +18,13 @@ export class ConceptDataSource extends BaseOpenMRSDataSource { const urlParts = searchUrl.split('searchType=fuzzy'); searchUrl = `${urlParts[0]}searchType=fuzzy&class=${config.class}&${urlParts[1]}`; } else { - return openmrsFetch(searchTerm ? `${searchUrl}&q=${searchTerm}` : searchUrl).then(({ data }) => { - return data.results.filter( - (concept) => concept.conceptClass && config.class.includes(concept.conceptClass.uuid), - ); - }); + return openmrsFetch(searchTerm ? `${searchUrl}&q=${searchTerm}` : `${searchUrl}&q=${'Diagnosis'}`).then( + ({ data }) => { + return data.results.filter( + (concept) => concept.conceptClass && config.class.includes(concept.conceptClass.uuid), + ); + }, + ); } } diff --git a/src/form-engine.test.tsx b/src/form-engine.test.tsx index 2af05c5a8..05d046e39 100644 --- a/src/form-engine.test.tsx +++ b/src/form-engine.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import dayjs from 'dayjs'; import userEvent from '@testing-library/user-event'; -import { act, cleanup, render, screen, within } from '@testing-library/react'; +import { act, cleanup, render, screen, waitFor, within } from '@testing-library/react'; import { ExtensionSlot, OpenmrsDatePicker, @@ -44,14 +44,18 @@ import viralLoadStatusForm from '__mocks__/forms/rfe-forms/viral-load-status-for import readOnlyValidationForm from '__mocks__/forms/rfe-forms/read-only-validation-form.json'; import jsExpressionValidationForm from '__mocks__/forms/rfe-forms/js-expression-validation-form.json'; import hidePagesAndSectionsForm from '__mocks__/forms/rfe-forms/hide-pages-and-sections-form.json'; +import diagnosisForm from '__mocks__/forms/rfe-forms/diagnosis-test-form.json'; import FormEngine from './form-engine.component'; import { type SessionMode } from './types'; +import { use } from 'i18next'; const patientUUID = '8673ee4f-e2ab-4077-ba55-4980f408773e'; const visit = mockVisit; const formsResourcePath = when((url: string) => url.includes(`${restBaseUrl}/form/`)); const clobDataResourcePath = when((url: string) => url.includes(`${restBaseUrl}/clobdata/`)); +const conceptResourcePath = when((url: string) => url.includes(`${restBaseUrl}/concept/`)); + global.ResizeObserver = require('resize-observer-polyfill'); const mockOpenmrsFetch = jest.mocked(openmrsFetch); @@ -79,6 +83,53 @@ mockOpenmrsDatePicker.mockImplementation(({ id, labelText, value, onChange, isIn when(mockOpenmrsFetch).calledWith(formsResourcePath).mockReturnValue({ data: demoHtsOpenmrsForm }); when(mockOpenmrsFetch).calledWith(clobDataResourcePath).mockReturnValue({ data: demoHtsForm }); +jest.mock('./registry/registry', () => { + const originalModule = jest.requireActual('./registry/registry'); + return { + ...originalModule, + getRegisteredDataSource: jest.fn().mockResolvedValue({ + fetchData: jest.fn().mockImplementation((...args) => { + if (args[1].class?.length && !args[1].referencedValue.key) { + // concept DS + return Promise.resolve([ + { + uuid: 'stage-1-uuid', + display: 'stage 1', + }, + { + uuid: 'stage-2-uuid', + display: 'stage 2', + }, + ]); + } + + // location DS + return Promise.resolve([ + { + uuid: 'aaa-1', + display: 'Kololo', + }, + { + uuid: 'aaa-2', + display: 'Naguru', + }, + { + uuid: 'aaa-3', + display: 'Muyenga', + }, + ]); + }), + fetchSingleItem: jest.fn().mockImplementation((uuid: string) => { + return Promise.resolve({ + uuid, + display: 'stage 1', + }); + }), + toUuidAndDisplay: (data) => data, + }), + }; +}); + jest.mock('../src/api', () => { const originalModule = jest.requireActual('../src/api'); @@ -1047,6 +1098,86 @@ describe('Form engine component', () => { }); }); + describe('Diagnisis field', () => { + it('should test addition of a diagnosis', async () => { + await act(async () => { + renderForm(null, diagnosisForm); + }); + const addButtons = screen.getAllByRole('button', { name: 'Add' }); + expect(addButtons.length).toBeGreaterThan(0); + screen.debug(addButtons); + + await user.click(addButtons[0]); + + await waitFor(() => { + expect(screen.getAllByRole('combobox', { name: /^test diagnosis 1$/i }).length).toEqual(2); + }); + + expect(screen.getByRole('button', { name: /Remove/i })).toBeInTheDocument(); + }); + + it('should render diagnosis field', async () => { + await act(async () => { + renderForm(null, diagnosisForm); + }); + const initialDiagnosis = screen.getAllByRole('combobox', { name: /test diagnosis 1|test diagnosis 2/i }); + expect(initialDiagnosis.length).toBe(2); + }); + + it('should save diagnosis field on form submission', async () => { + await act(async () => { + renderForm(null, diagnosisForm); + }); + const saveEncounterMock = jest.spyOn(api, 'saveEncounter'); + + const combobox = await screen.findByRole('combobox', { name: /test diagnosis 1/i }); + expect(combobox).toBeInTheDocument(); + + await userEvent.click(combobox); + await waitFor(() => { + expect(screen.getByText('stage 1')).toBeInTheDocument(); + expect(screen.getByText('stage 2')).toBeInTheDocument(); + }); + await user.click(screen.getByText('stage 1')); + await user.click(screen.getByRole('button', { name: /save/i })); + expect(saveEncounterMock).toHaveBeenCalledTimes(1); + const [_, encounter] = saveEncounterMock.mock.calls[0]; + expect(encounter.diagnoses.length).toBe(1); + expect(encounter.diagnoses[0]).toEqual({ + patient: '8673ee4f-e2ab-4077-ba55-4980f408773e', + condition: null, + diagnosis: { + coded: 'stage-1-uuid', + }, + certainty: 'CONFIRMED', + rank: 1, + formFieldPath: `rfe-forms-diagnosis1`, + formFieldNamespace: 'rfe-forms', + }); + }); + + it('should test removing of a diagnosis field', async () => { + await act(async () => { + renderForm(null, diagnosisForm); + }); + + const addButtons = screen.getAllByRole('button', { name: 'Add' }); + expect(addButtons.length).toBeGreaterThan(0); + screen.debug(addButtons); + + await user.click(addButtons[0]); + + await waitFor(() => { + expect(screen.getAllByRole('combobox', { name: /^test diagnosis 1$/i }).length).toEqual(2); + }); + const removeButton = screen.getByRole('button', { name: /Remove/i }); + + await user.click(removeButton); + + expect(removeButton).not.toBeInTheDocument(); + }); + }); + function renderForm(formUUID, formJson, intent?: string, mode?: SessionMode) { render( order.orderNumber); - if (saveOrders.length) { + const savedOrders = savedEncounter.orders.map((order) => order.orderNumber); + const savedDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); + if (savedOrders.length) { showSnackbar({ title: translateFn('ordersSaved', 'Order(s) saved successfully'), - subtitle: saveOrders.join(', '), + subtitle: savedOrders.join(', '), + kind: 'success', + isLowContrast: true, + }); + } + // handle diagnoses + if (savedDiagnoses.length) { + showSnackbar({ + title: translateFn('diagnosisSaved', 'Diagnosis(es) saved successfully'), + subtitle: savedDiagnoses.join(', '), kind: 'success', isLowContrast: true, }); diff --git a/src/processors/encounter/encounter-processor-helper.ts b/src/processors/encounter/encounter-processor-helper.ts index 4c5be1055..7ccd0323c 100644 --- a/src/processors/encounter/encounter-processor-helper.ts +++ b/src/processors/encounter/encounter-processor-helper.ts @@ -17,6 +17,7 @@ import { DefaultValueValidator } from '../../validators/default-value-validator' import { cloneRepeatField } from '../../components/repeat/helpers'; import { assignedOrderIds } from '../../adapters/orders-adapter'; import { type OpenmrsResource } from '@openmrs/esm-framework'; +import { assignedDiagnosesIds } from '../../adapters/encounter-diagnosis-adapter'; export function prepareEncounter( context: FormContextProps, @@ -30,6 +31,8 @@ export function prepareEncounter( const obsForSubmission = []; prepareObs(obsForSubmission, allFormFields); const ordersForSubmission = prepareOrders(allFormFields); + const diagnosesForSubmission = prepareDiagnosis(allFormFields); + let encounterForSubmission: OpenmrsEncounter = {}; if (encounter) { @@ -59,6 +62,7 @@ export function prepareEncounter( } encounterForSubmission.obs = obsForSubmission; encounterForSubmission.orders = ordersForSubmission; + encounterForSubmission.diagnoses = diagnosesForSubmission; } else { encounterForSubmission = { patient: patient.id, @@ -77,6 +81,7 @@ export function prepareEncounter( }, visit: visit?.uuid, orders: ordersForSubmission, + diagnoses: diagnosesForSubmission, }; } return encounterForSubmission; @@ -314,6 +319,33 @@ export async function hydrateRepeatField( }), ); } + + const unMappedDiagnoses = encounter.diagnoses.filter((diagnosis) => { + return ( + !diagnosis.voided && + !assignedDiagnosesIds.includes(diagnosis?.diagnosis?.coded.uuid) && + diagnosis.formFieldPath.startsWith(`rfe-forms-${field.id}_`) + ); + }); + + if (field.type === 'diagnosis') { + return Promise.all( + unMappedDiagnoses.map(async (diagnosis) => { + const idSuffix = parseInt(diagnosis.formFieldPath.split('_')[1]); + const clone = cloneRepeatField(field, diagnosis, idSuffix); + 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) => { @@ -332,3 +364,12 @@ export async function hydrateRepeatField( }), ).then((results) => results.flat()); } + +function prepareDiagnosis(fields: FormField[]) { + const diagnoses = fields + .filter((field) => field.type === 'diagnosis' && hasSubmission(field)) + .map((field) => field.meta.submission.newValue || field.meta.submission.voidedValue) + .filter((o) => o); + + return diagnoses; +} diff --git a/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts b/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts index 9a4c3d850..2a5632bf7 100644 --- a/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +++ b/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts @@ -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 { EncounterDiagnosisAdapter } from '../../adapters/encounter-diagnosis-adapter'; import { type FormFieldValueAdapter } from '../../types'; export const inbuiltFieldValueAdapters: RegistryItem[] = [ @@ -61,4 +62,8 @@ export const inbuiltFieldValueAdapters: RegistryItem[] = type: 'patientIdentifier', component: PatientIdentifierAdapter, }, + { + type: 'diagnosis', + component: EncounterDiagnosisAdapter, + }, ]; diff --git a/src/transformers/default-schema-transformer.ts b/src/transformers/default-schema-transformer.ts index b39510a79..91559dad2 100644 --- a/src/transformers/default-schema-transformer.ts +++ b/src/transformers/default-schema-transformer.ts @@ -148,6 +148,9 @@ function transformByType(question: FormField) { ? 'date' : question.questionOptions.rendering; break; + case 'diagnosis': + handleDiagnosis(question); + break; } } @@ -276,3 +279,31 @@ function handleQuestionsWithObsComments(sectionQuestions: Array): Arr return augmentedQuestions; } + +function handleDiagnosis(question: FormField) { + if ( + ('dataSource' in question.questionOptions && question.questionOptions['dataSource'] === 'diagnoses') || + question.type === 'diagnosis' + ) { + question.questionOptions.datasource = { + name: 'problem_datasource', + config: { + class: question.questionOptions.diagnosis?.conceptClasses, + }, + }; + if (question.questionOptions.diagnosis?.conceptSet) { + question.questionOptions = { + ...question.questionOptions, + concept: question.questionOptions.diagnosis?.conceptSet, + datasource: { + name: 'problem_datasource', + config: { + useSetMembersByConcept: true, + }, + }, + }; + } + + delete question.questionOptions['dataSource']; + } +} diff --git a/src/types/domain.ts b/src/types/domain.ts index b79ffda62..39f12d52c 100644 --- a/src/types/domain.ts +++ b/src/types/domain.ts @@ -12,6 +12,7 @@ export interface OpenmrsEncounter { visit?: OpenmrsResource | string; encounterProviders?: Array>; form?: OpenmrsFormResource; + diagnoses?: Array; } export interface OpenmrsObs extends OpenmrsResource { @@ -191,3 +192,34 @@ export interface PatientIdentifier { location?: string; preferred?: boolean; } + +export interface DiagnosisPayload { + patient: string; + condition: null; + diagnosis: { + coded: string; + }; + certainty: string; + rank: number; + formFieldNamespace?: string; + formFieldPath?: string; + uuid?: string; + encounter?: string; +} + +export interface Diagnosis { + encounter: string; + patient: string; + diagnosis: { + coded: { + uuid: string; + }; + }; + certainty: string; + rank: number; + display: string; + voided: boolean; + uuid: string; + formFieldNamespace?: string; + formFieldPath?: string; +} diff --git a/src/types/schema.ts b/src/types/schema.ts index 1eb14a931..be00e3154 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -192,6 +192,12 @@ export interface FormQuestionOptions { comment?: string; orientation?: 'vertical' | 'horizontal'; shownCommentOptions?: { validators?: Array>; hide?: { hideWhenExpression: string } }; + diagnosis?: { + rank?: number; + isConfirmed?: boolean; + conceptClasses?: Array; + conceptSet?: string; + }; } export interface QuestionAnswerOption {