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-3049: adding evaluated historical expression support #226

Merged
merged 18 commits into from
May 9, 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
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';
samuelmale marked this conversation as resolved.
Show resolved Hide resolved

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;
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to receive a null or undefined argument for value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it is not. The following evaluations need to be truthy before the component is called.
encounterContext?.previousEncounter && (previousFieldValue || historicalValue) && (isTrue(fieldDescriptor.questionOptions.enablePreviousValue) || fieldDescriptor.historicalExpression)Ï

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
Loading