Skip to content

Commit

Permalink
(feat) O3-3049: Add support for historical expression (#226)
Browse files Browse the repository at this point in the history
* Initial commit(will remove the date bug fix)

* cleanup

* Getting the HD object to evaluate
tt
t

* aligning Hxp with previous value

* more cleanup

* resolving logic

* revamped transformer, migrated historical data source class

* combine hxp and previous value logic

* Added tests

* PR resolutions

* Fix CI build failure
  • Loading branch information
arodidev authored May 9, 2024
1 parent fe272f9 commit 30cf1b1
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 22 deletions.
170 changes: 170 additions & 0 deletions __mocks__/forms/rfe-forms/historical-expressions-form.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{
"name": "COVID Assessment Form",
"version": "1",
"published": true,
"retired": false,
"pages": [
{
"label": "COVID Assessment",
"sections": [
{
"label": "Assessment Details",
"isExpanded": "true",
"questions": [
{
"label": "Reasons for assessment",
"type": "obs",
"historicalExpression": "HD.getObject('prevEnc').getValue('ae46f4b1-c15d-4bba-ab41-b9157b82b0ce')",
"questionOptions": {
"rendering": "checkbox",
"concept": "ae46f4b1-c15d-4bba-ab41-b9157b82b0ce",
"answers": [
{
"concept": "1068AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"label": "Symptomatic",
"conceptMappings": [
{
"type": "AMPATH",
"value": "1068"
},
{
"type": "SNOMED-CT",
"value": "264931009"
}
]
},
{
"concept": "0ed2e3ca-b9a6-4ff6-ac74-4d4cd9520acc",
"label": "RDT confirmatory",
"conceptMappings": []
},
{
"concept": "f974e267-feeb-42be-9d37-61554dad16b1",
"label": "Voluntary",
"conceptMappings": []
},
{
"concept": "1cee0ab3-bf06-49e9-a49c-baf261620c67",
"label": "Post-mortem",
"conceptMappings": []
},
{
"concept": "e0f1584a-cc8b-48e9-980f-64d9f724caf8",
"label": "Quarantine",
"conceptMappings": []
},
{
"concept": "ad8be6bf-ced7-4390-a6af-c5acebeac216",
"label": "Follow-up",
"conceptMappings": []
},
{
"concept": "30320fb8-b29b-443f-98cf-f3ef491f8947",
"label": "Health worker",
"conceptMappings": []
},
{
"concept": "38769c82-a3d3-4714-97b7-015726cdb209",
"label": "Other frontline worker",
"conceptMappings": []
},
{
"concept": "f8c9c2cc-3070-444e-8818-26fb8100bb78",
"label": "Travel out of country",
"conceptMappings": []
},
{
"concept": "677f4d21-7293-4810-abe6-57a192acde57",
"label": "Entry into a country",
"conceptMappings": []
},
{
"concept": "8a6ab892-1b1d-4ad9-82da-c6c38ee8fcfb",
"label": "Surveillance",
"conceptMappings": []
},
{
"concept": "5340f478-ec5d-41e6-bc62-961c52014d4d",
"label": "Contact of a case",
"conceptMappings": []
},
{
"concept": "5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"label": "Other",
"conceptMappings": [
{
"type": "PIH-Malawi",
"value": "6408"
},
{
"type": "org.openmrs.module.mdrtb",
"value": "OTHER"
},
{
"type": "CIEL",
"value": "5622"
},
{
"type": "SNOMED-MVP",
"value": "56221000105001"
},
{
"type": "PIH",
"value": "5622"
},
{
"type": "AMPATH",
"value": "5622"
},
{
"type": "SNOMED-CT",
"value": "74964007"
}
]
}
]
},
"id": "reasonsForTesting",
"behaviours": [
{
"intent": "*",
"required": "true",
"unspecified": "true",
"hide": {
"hideWhenExpression": "false"
},
"validators": []
},
{
"intent": "COVID_LAB_ASSESSMENT_EMBED",
"required": "true",
"unspecified": "true",
"hide": {
"hideWhenExpression": "false"
},
"validators": []
}
]
}
]
}
]
}
],
"availableIntents": [
{
"intent": "*",
"display": "COVID Assessment"
},
{
"intent": "COVID_LAB_ASSESSMENT_EMBED",
"display": "Covid Case Form"
}
],
"processor": "EncounterFormProcessor",
"uuid": "f5fb6bc4-6fc3-3462-a191-2fff0896bab3",
"referencedForms": [],
"encounterType": "253a43d3-c99e-415c-8b78-ee7d4d3c1d54",
"encounter": "COVID Case Assessment",
"allowUnspecifiedAll": true
}
89 changes: 89 additions & 0 deletions __mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"uuid": "82649039-540d-45e8-b935-2edf3ac68449",
"encounterDatetime": "2024-05-01T09:34:18.000+0000",
"encounterType": {
"uuid": "253a43d3-c99e-415c-8b78-ee7d4d3c1d54",
"display": "COVID Case Assessment",
"name": "COVID Case Assessment",
"description": "This encounter type is shared across the COVID Assessment Form/COVID Outcome Form and COVID Lab Result Form",
"retired": false,
"links": [
{
"rel": "self",
"uri": "https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encountertype/253a43d3-c99e-415c-8b78-ee7d4d3c1d54",
"resourceAlias": "encountertype"
},
{
"rel": "full",
"uri": "https://ohri-dev.globalhealthapp.net/openmrs/ws/rest/v1/encountertype/253a43d3-c99e-415c-8b78-ee7d4d3c1d54?v=full",
"resourceAlias": "encountertype"
}
],
"resourceVersion": "1.8"
},
"location": {
"uuid": "28066518-a0e5-4331-b23e-7b40f96d733a",
"name": "MCH Clinic"
},
"patient": {
"uuid": "b280078a-c0ce-443b-9997-3c66c63ec2f8",
"display": "100000Y - John Doe"
},
"encounterProviders": [
{
"uuid": "72d1879e-f949-48ba-bdbe-9ea157ab5c20",
"provider": {
"uuid": "bc450226-4138-40b7-ad88-9c98df687738",
"name": "Super User"
}
}
],
"orders": [],
"obs": [
{
"uuid": "810d08df-9e14-494c-9c02-eada18af90ed",
"obsDatetime": "2024-05-01T09:34:18.000+0000",
"voided": false,
"groupMembers": null,
"formFieldNamespace": "rfe-forms",
"formFieldPath": "rfe-forms-dateOfAssessment",
"concept": {
"uuid": "160753AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"name": {
"uuid": "f415bf25-2008-3ac2-8f0e-46e75648e30d",
"name": "Date of event"
}
},
"value": "2024-01-08T00:00:00.000+0000"
},
{
"uuid": "f03bc18e-b4bc-432c-bffc-47daff58892b",
"obsDatetime": "2024-05-01T09:34:18.000+0000",
"voided": false,
"groupMembers": null,
"formFieldNamespace": "rfe-forms",
"formFieldPath": "rfe-forms-reasonsForTesting",
"concept": {
"uuid": "ae46f4b1-c15d-4bba-ab41-b9157b82b0ce",
"name": {
"uuid": "8b31f49b-04cf-3c31-b9da-149f262aa7b6",
"name": "What is the reason for COVID-19 testing?"
}
},
"value": {
"uuid": "677f4d21-7293-4810-abe6-57a192acde57",
"name": {
"uuid": "3f3b2df5-fe22-3833-b2e9-22368c88f6de",
"name": "Entry into a country"
},
"names": [
{
"uuid": "3f3b2df5-fe22-3833-b2e9-22368c88f6de",
"conceptNameType": "FULLY_SPECIFIED",
"name": "Entry into a country"
}
]
}
}
]
}
2 changes: 1 addition & 1 deletion src/components/inputs/date/date.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const DateField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
}, []);

useEffect(() => {
if (encounterContext?.previousEncounter && !isTrue(question.questionOptions.usePreviousValueDisabled)) {
if (encounterContext?.previousEncounter && isTrue(question.questionOptions.enablePreviousValue)) {
let prevValue = handler?.getPreviousValue(question, encounterContext?.previousEncounter, fields);

if (!isEmpty(prevValue?.value)) {
Expand Down
12 changes: 7 additions & 5 deletions src/components/inputs/multi-select/multi-select.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, handler, pr
};

useEffect(() => {
if (!isEmpty(previousValue) && Array.isArray(previousValue)) {
const valuesToSet = previousValue.map((eachItem) => eachItem.value);
setFieldValue(question.id, valuesToSet);
onChange(question.id, valuesToSet, setErrors, setWarnings);
question.value = handler?.handleFieldSubmission(question, valuesToSet, encounterContext);
if (!isEmpty(previousValue)) {
const previousValues = Array.isArray(previousValue)
? previousValue.map((item) => item.value)
: [previousValue.value];
setFieldValue(question.id, previousValues);
onChange(question.id, previousValues, setErrors, setWarnings);
question.value = handler?.handleFieldSubmission(question, previousValues, encounterContext);
}
}, [previousValue]);

Expand Down
32 changes: 28 additions & 4 deletions src/components/section/form-section.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import { useField } from 'formik';
import type { FormField, FormFieldProps, previousValue, SubmissionHandler } from '../../types';
import { ErrorBoundary } from 'react-error-boundary';
import { ToastNotification } from '@carbon/react';
import { formatPreviousValueDisplayText, getFieldControlWithFallback, isUnspecifiedSupported } from './helpers';
import {
formatPreviousValueDisplayText,
getFieldControlWithFallback,
isUnspecifiedSupported,
extractObsValueAndDisplay,
} from './helpers';
import { getRegisteredFieldSubmissionHandler } from '../../registry/registry';
import { isTrue } from '../../utils/boolean-utils';
import { FormContext } from '../../form-context';
import PreviousValueReview from '../previous-value-review/previous-value-review.component';
import Tooltip from '../inputs/tooltip/tooltip.component';
import UnspecifiedField from '../inputs/unspecified/unspecified.component';
import styles from './form-section.scss';
import { evaluateExpression } from '../../utils/expression-runner';

interface FieldComponentMap {
fieldComponent: React.ComponentType<FormFieldProps>;
Expand Down Expand Up @@ -47,7 +53,24 @@ const FormSection = ({ fields, onFieldChange }) => {
.map((entry, index) => {
const { fieldComponent: FieldComponent, fieldDescriptor, handler } = entry;
const rendering = fieldDescriptor.questionOptions.rendering;
const previousFieldValue = encounterContext.previousEncounter

const historicalValue = fieldDescriptor.historicalExpression
? evaluateExpression(
fieldDescriptor.historicalExpression,
{ value: fieldDescriptor, type: 'field' },
fieldsFromEncounter,
encounterContext.initValues,
{
mode: encounterContext.sessionMode,
patient: encounterContext.patient,
previousEncounter: encounterContext.previousEncounter,
},
)
: null;

const previousFieldValue = historicalValue
? extractObsValueAndDisplay(fieldDescriptor, historicalValue)
: encounterContext.previousEncounter
? handler?.getPreviousValue(fieldDescriptor, encounterContext.previousEncounter, fieldsFromEncounter)
: null;

Expand Down Expand Up @@ -96,8 +119,9 @@ const FormSection = ({ fields, onFieldChange }) => {
)}
</div>
{encounterContext?.previousEncounter &&
previousFieldValue &&
!isTrue(fieldDescriptor.questionOptions.usePreviousValueDisabled) && (
(previousFieldValue || historicalValue) &&
(isTrue(fieldDescriptor.questionOptions.enablePreviousValue) ||
fieldDescriptor.historicalExpression) && (
<div className={styles.previousValue}>
<PreviousValueReview
previousValue={previousFieldValue}
Expand Down
31 changes: 29 additions & 2 deletions src/components/section/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { formatDate } from '@openmrs/esm-framework';
import { getRegisteredControl } from '../../registry/registry';
import { isTrue } from '../../utils/boolean-utils';
import { type FormField } from '../../types';
import { type OpenmrsObs, type FormField } from '../../types';
import { parseToLocalDateTime } from '../../utils/form-helper';
import dayjs from 'dayjs';

/**
* Retrieves the appropriate field control for a question, considering missing concepts.
Expand Down Expand Up @@ -46,10 +48,35 @@ function previousValueDisplayForCheckbox(previosValueItems: Object[]): String {
export const formatPreviousValueDisplayText = (question: FormField, value: any) => {
switch (question.questionOptions.rendering) {
case 'date':
if (value instanceof Date) {
return formatDate(value);
}
return formatDate(new Date(value?.display)) || formatDate(value?.value);
case 'checkbox':
return Array.isArray(value) ? previousValueDisplayForCheckbox(value) : null;
return Array.isArray(value) ? previousValueDisplayForCheckbox(value) : value.display;
default:
return value?.display;
}
};

export const extractObsValueAndDisplay = (field: FormField, obs: OpenmrsObs) => {
const rendering = field.questionOptions.rendering;

if (typeof obs.value === 'string' || typeof obs.value === 'number') {
if (rendering === 'date' || rendering === 'datetime') {
const dateObj = parseToLocalDateTime(`${obs.value}`);
return { value: dateObj, display: dayjs(dateObj).format('YYYY-MM-DD HH:mm') };
}
return { value: obs.value, display: obs.value };
} else if (['toggle', 'checkbox'].includes(rendering)) {
return {
value: obs.value?.uuid,
display: obs.value?.name?.name,
};
} else {
return {
value: obs.value?.uuid,
display: field.questionOptions.answers?.find((option) => option.concept === obs.value?.uuid)?.label,
};
}
};
Loading

0 comments on commit 30cf1b1

Please sign in to comment.