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-3007: Add support for saving patient identifiers in forms #202

Merged
merged 4 commits into from
Apr 25, 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
22 changes: 21 additions & 1 deletion src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
import { encounterRepresentation } from '../constants';
import { OpenmrsForm, ProgramEnrollmentPayload } from '../types';
import { OpenmrsForm, PatientIdentifier, ProgramEnrollmentPayload } from '../types';
import { isUuid } from '../utils/boolean-utils';

export function saveEncounter(abortController: AbortController, payload, encounterUuid?: string) {
Expand Down Expand Up @@ -189,3 +189,23 @@ export function updateProgramEnrollment(
signal: abortController.signal,
});
}

export function savePatientIdentifier(patientIdentifier:PatientIdentifier, patientUuid: string){
let url: string;

if (patientIdentifier.uuid) {
url = `${restBaseUrl}/patient/${patientUuid}/identifier/${patientIdentifier.uuid}`;
} else {
url = `${restBaseUrl}/patient/${patientUuid}/identifier`;
}

return openmrsFetch(url, {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(patientIdentifier),
});
}


81 changes: 64 additions & 17 deletions src/components/encounter/encounter-form.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import { SessionLocation, showToast, useLayoutType, Visit } from '@openmrs/esm-framework';
import { SessionLocation, showToast, showSnackbar, useLayoutType, Visit } from '@openmrs/esm-framework';
import { codedTypes, ConceptFalse, ConceptTrue } from '../../constants';
import {
FormField,
FormPage as FormPageProps,
FormSchema,
OpenmrsEncounter,
PatientIdentifier,
QuestionAnswerOption,
RepeatObsGroupCounter,
SessionMode,
Expand Down Expand Up @@ -33,6 +34,8 @@ import { useEncounterRole } from '../../hooks/useEncounterRole';
import { useConcepts } from '../../hooks/useConcepts';
import { useFormFieldHandlers } from '../../hooks/useFormFieldHandlers';
import { useFormFieldValidators } from '../../hooks/useFormFieldValidators';
import { saveIdentifier } from '../../utils/patient-identifier-helper';
import { useTranslation } from 'react-i18next';

interface EncounterFormProps {
formJson: FormSchema;
Expand Down Expand Up @@ -81,6 +84,7 @@ export const EncounterForm: React.FC<EncounterFormProps> = ({
isSubmitting,
setIsSubmitting,
}) => {
const { t } = useTranslation();
const [fields, setFields] = useState<Array<FormField>>([]);
const [encounterLocation, setEncounterLocation] = useState(null);
const [encounterDate, setEncounterDate] = useState(formSessionDate);
Expand Down Expand Up @@ -557,24 +561,67 @@ export const EncounterForm: React.FC<EncounterFormProps> = ({
};
}

if (encounterForSubmission.obs?.length || encounterForSubmission.orders?.length) {
const ac = new AbortController();
return saveEncounter(ac, encounterForSubmission, encounter?.uuid).then((response) => {
const encounter = response.data;
const fileFields = fields?.filter((field) => field?.questionOptions.rendering === 'file');
const saveAttachmentPromises = fileFields.map((field) => {
return saveAttachment(
encounter?.patient.uuid,
field,
field?.questionOptions.concept,
new Date().toISOString(),
encounter?.uuid,
ac,
);
let formPatientIdentifiers = '';
const patientIdentifierFields = fields.filter((field) => field.type === 'patientIdentifier');
const patientIdentifierPromises = patientIdentifierFields.map((field) => {
const identfier: PatientIdentifier = {
identifier: field.value,
identifierType: field.questionOptions.identifierType,
location: encounterLocation,
};
return saveIdentifier(encounterContext.patient, identfier);
});

return Promise.all(patientIdentifierPromises)
.then(() => {
for (let i = 0; i < patientIdentifierFields.length; i++) {
formPatientIdentifiers += patientIdentifierFields[i].value;
if (i < patientIdentifierFields.length - 1) {
formPatientIdentifiers += ', ';
}
}
if (patientIdentifierFields.length) {
showSnackbar({
title: t('identifierCreated', 'Identifier Created'),
subtitle: t(
'identifierCreatedDescription',
`Patient Identifier(s) ${formPatientIdentifiers} successfully created!`,
),
kind: 'success',
isLowContrast: true,
});
}
saveEncounterWithAttachments(encounterForSubmission);
})
.catch((error) => {
setIsSubmitting(false);
showSnackbar({
title: t('errorDescriptionTitle', 'Error on saving form'),
subtitle: t('errorDescription', error.message),
kind: 'error',
isLowContrast: false,
});
return Promise.all(saveAttachmentPromises).then(() => response);
return Promise.reject(error);
});
}
};

const saveEncounterWithAttachments = (encounterForSubmission: OpenmrsEncounter) => {
const ac = new AbortController();
return saveEncounter(ac, encounterForSubmission, encounter?.uuid).then((response) => {
const encounter = response.data;
const fileFields = fields?.filter((field) => field?.questionOptions.rendering === 'file');
const saveAttachmentPromises = fileFields.map((field) => {
return saveAttachment(
encounter?.patient.uuid,
field,
field?.questionOptions.concept,
new Date().toISOString(),
encounter?.uuid,
ac,
);
});
return Promise.all(saveAttachmentPromises).then(() => response);
});
};

const onFieldChange = (
Expand Down
2 changes: 1 addition & 1 deletion src/components/section/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function previousValueDisplayForCheckbox(previosValueItems: Object[]): String {
export const formatPreviousValueDisplayText = (question: FormField, value: any) => {
switch (question.questionOptions.rendering) {
case 'date':
return formatDate(new Date(value?.display));
return formatDate(value) || formatDate(new Date(value?.display));
case 'checkbox':
return Array.isArray(value) ? previousValueDisplayForCheckbox(value) : null;
default:
Expand Down
3 changes: 2 additions & 1 deletion src/form-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { LayoutType } from '@openmrs/esm-framework';
import { FormField, OpenmrsEncounter, SessionMode, SubmissionHandler } from './types';
import { FormField, OpenmrsEncounter, PatientIdentifier, SessionMode, SubmissionHandler } from './types';

type FormContextProps = {
values: Record<string, any>;
Expand Down Expand Up @@ -30,6 +30,7 @@ export interface EncounterContext {
setEncounterLocation(value: any): void;
initValues?: Record<string, any>;
setObsGroupCounter?: any;
patientIdentifier?: PatientIdentifier;
}

export const FormContext = React.createContext<FormContextProps | undefined>(undefined);
4 changes: 2 additions & 2 deletions src/hooks/useInitialValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useInitialValues(
const [isEncounterBindingComplete, setIsEncounterBindingComplete] = useState(
encounterContext.sessionMode === 'enter',
);
const encounterContextInitializableTypes = ['encounterProvider', 'encounterDatetime', 'encounterLocation'];
const encounterContextInitializableTypes = ['encounterProvider', 'encounterDatetime', 'encounterLocation', 'patientIdentifier'];

useEffect(() => {
const asyncItemsKeys = Object.keys(asyncInitValues ?? {});
Expand Down Expand Up @@ -63,7 +63,7 @@ export function useInitialValues(
!field.questionOptions.repeatOptions?.isCloned && repeatableFields.push(field);
return;
}
let existingVal = formFieldHandlers[field.type]?.getInitialValue(encounter, field, formFields);
let existingVal = formFieldHandlers[field.type]?.getInitialValue(encounter, field, formFields, encounterContext);

if (isEmpty(existingVal) && !isEmpty(field.questionOptions.defaultValue)) {
existingVal = inferInitialValueFromDefaultFieldValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EncounterDatetimeHandler } from '../../submission-handlers/encounterDat
import { EncounterLocationSubmissionHandler } from '../../submission-handlers/encounterLocationHandler';
import { EncounterProviderHandler } from '../../submission-handlers/encounterProviderHandler';
import { SubmissionHandler } from '../../types';
import { PatientIdentifierHandler } from '../../submission-handlers/patientIdentifierHandler';
import { RegistryItem } from '../registry';

/**
Expand Down Expand Up @@ -34,4 +35,9 @@ export const inbuiltFieldSubmissionHandlers: Array<RegistryItem<SubmissionHandle
component: EncounterProviderHandler,
type: 'encounterProvider',
},
{
name: 'PatientIdentifierHandler',
component: PatientIdentifierHandler,
type: 'patientIdentifier',
},
];
5 changes: 2 additions & 3 deletions src/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface FieldSubmissionHandlerRegistration extends ComponentRegistratio
type: string;
}

export interface FormSchemaTransformerRegistration extends ComponentRegistration<FormSchemaTransformer> {}

export interface FormsRegistryStoreState {
controls: CustomControlRegistration[];
Expand All @@ -49,7 +48,7 @@ export interface FormsRegistryStoreState {
postSubmissionActions: ComponentRegistration<PostSubmissionAction>[];
dataSources: ComponentRegistration<DataSource<any>>[];
expressionHelpers: Record<string, Function>;
formSchemaTransformers: FormSchemaTransformerRegistration[];
formSchemaTransformers: ComponentRegistration<FormSchemaTransformer>[];
}

interface FormRegistryCache {
Expand Down Expand Up @@ -96,7 +95,7 @@ export function registerExpressionHelper(name: string, fn: Function) {
getFormsStore().expressionHelpers[name] = fn;
}

export function registereformSchemaTransformers(registration: FormSchemaTransformerRegistration) {
export function registereformSchemaTransformers(registration: ComponentRegistration<FormSchemaTransformer>) {
const store = getFormsStore();
const existingIndex = store.formSchemaTransformers.findIndex((reg) => reg.name === registration.name);

Expand Down
25 changes: 25 additions & 0 deletions src/submission-handlers/patientIdentifierHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EncounterContext, FormField, OpenmrsEncounter, SubmissionHandler } from '..';
import { getPatientLatestIdentifier } from '../utils/patient-identifier-helper';

export const PatientIdentifierHandler: SubmissionHandler = {
handleFieldSubmission: (field: FormField, value: any, context: EncounterContext) => {
return value;
},
getInitialValue: (
encounter: OpenmrsEncounter,
field: FormField,
allFormFields: Array<FormField>,
context: EncounterContext,
) => {
const patientIdentifier = getPatientLatestIdentifier(context.patient, field.questionOptions.identifierType);
return patientIdentifier?.value;
},

getDisplayValue: (field: FormField, value: any) => {
return value;
},
getPreviousValue: (field: FormField, encounter: OpenmrsEncounter, allFormFields: Array<FormField>) => {
return null;
},

};
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export interface FormQuestionOptions {
isSearchable?: boolean;
workspaceName?: string;
buttonLabel?: string;
identifierType?: string;
}

export type SessionMode = 'edit' | 'enter' | 'view' | 'embedded-view';
Expand Down Expand Up @@ -359,6 +360,14 @@ export interface ProgramEnrollmentPayload {
location: string;
}

export interface PatientIdentifier {
uuid?: string;
identifier: string;
identifierType?: string;
location?: string;
preferred?: boolean;
}

/**
* A form schema transformer is used to bridge the gap caused by different variations of form schemas
* in the OpenMRS JSON schema-based form-entry world. It fine-tunes custom schemas to be compliant
Expand Down
21 changes: 21 additions & 0 deletions src/utils/patient-identifier-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { savePatientIdentifier} from "../api/api";
import { PatientIdentifier } from "../types";

export const saveIdentifier = (patient: fhir.Patient, patientIdentifier:PatientIdentifier) =>{
const identifier = getPatientLatestIdentifier(patient, patientIdentifier.identifierType);
if(identifier){
patientIdentifier.uuid = identifier.id;
}

return savePatientIdentifier(patientIdentifier, patient.id);
}

export const getPatientLatestIdentifier = (patient: fhir.Patient, identifierType: string) => {
const patientIdentifiers = patient.identifier;
return patientIdentifiers.find(identifier => {
if (identifier.type.coding && identifier.type.coding[0].code === identifierType) {
return true;
}
return false;
});
}
1 change: 0 additions & 1 deletion src/zscore-tests/bmi-age.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ describe('bmiForAge z-score', () => {
const weight = screen.getByRole('spinbutton', { name: /weight/i });

await user.type(height, '100');
await user.tab();
await user.type(weight, '45');
await user.tab();

Expand Down
1 change: 0 additions & 1 deletion src/zscore-tests/height-age.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ describe('heightForAge z-score', () => {
const weight = screen.getByRole('spinbutton', { name: /weight/i });

await user.type(height, '150');
await user.tab();
await user.type(weight, '45');
await user.tab();

Expand Down
1 change: 0 additions & 1 deletion src/zscore-tests/weight-height.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ describe('weightForHeight z-score', () => {
const weight = screen.getByRole('spinbutton', { name: /weight/i });

await user.type(height, '110');
await user.tab();
await user.type(weight, '45');
await user.tab();

Expand Down
Loading