-
Notifications
You must be signed in to change notification settings - Fork 62
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
base: main
Are you sure you want to change the base?
Conversation
Size Change: -263 kB (-17.32%) 👏 Total Size: 1.25 MB
ℹ️ View Unchanged
|
const saveDiagnoses = await EncounterFormManager.saveDiagnosis(fields, savedEncounter); | ||
if (saveDiagnoses) { | ||
showSnackbar({ | ||
title: t('encounterDiagnosisSaved', 'Encounter diagnosis saved successfully'), | ||
kind: 'success', | ||
isLowContrast: true, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the EncounterResource supports diagnoses as a create-able property shouldn't we be creating the diagnoses in a single transaction with the encounter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me add it the same way we handle orders
@CynthiaKamau Ping, are you still working on this? Any blockers? |
Yes, there some fixes that need to be done on the backend to complete the work |
@CynthiaKamau Please provide links to the work or issues you're talking about. If there is no linkable issue/PR/talk/Slack, please tell us what work needs to be completed before this can move forward. |
This is the link to the backend pr |
Ok @CynthiaKamau , looks like that backend PR is merged. Can we move forward with this? |
a40ba8d
to
2415c6d
Compare
Im working on it now |
e5de3c9
to
63ad98e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @CynthiaKamau! Are you planning on adding some test coverage?
@@ -300,6 +304,27 @@ export async function hydrateRepeatField( | |||
}), | |||
); | |||
} | |||
|
|||
//handle diagnoses | |||
const unMappedDiagnosis = encounter.diagnoses.filter((diagnosis) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const unMappedDiagnosis = encounter.diagnoses.filter((diagnosis) => { | |
const unMappedDiagnoses = encounter.diagnoses.filter((diagnosis) => { |
@@ -28,6 +29,7 @@ export function prepareEncounter( | |||
const obsForSubmission = []; | |||
prepareObs(obsForSubmission, formFields); | |||
const ordersForSubmission = prepareOrders(formFields); | |||
const diagnosisForSubmission = prepareDiagnosis(formFields); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const diagnosisForSubmission = prepareDiagnosis(formFields); | |
const diagnosesForSubmission = prepareDiagnosis(formFields); |
context: FormProcessorContextProps, | ||
): Promise<any> { | ||
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); | ||
const matchedDiagnosis = availableDiagnoses.diagnoses?.find( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const matchedDiagnosis = availableDiagnoses.diagnoses?.find( | |
const matchedDiagnoses = availableDiagnoses.diagnoses?.find( |
@@ -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 saveDiagnosis = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const saveDiagnosis = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); | |
const savedDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); |
@@ -237,3 +240,19 @@ function handleQuestionsWithObsComments(sectionQuestions: Array<FormField>): Arr | |||
|
|||
return augmentedQuestions; | |||
} | |||
|
|||
function handleDiagnosesDataSource(question: FormField) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would simply name this: handleDiagnosis(...)
bc33d5d
to
089214e
Compare
sourceObject: OpenmrsResource, | ||
context: FormProcessorContextProps, | ||
): Promise<any> { | ||
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); | |
const encounter = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); |
context: FormProcessorContextProps, | ||
): Promise<any> { | ||
const availableDiagnoses = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter); | ||
const matchedDiagnoses = availableDiagnoses.diagnoses.find( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const matchedDiagnoses = availableDiagnoses.diagnoses.find( | |
const matchedDiagnosis = availableDiagnoses.diagnoses.find( |
}, | ||
]; | ||
|
||
describe('EncounterDiagnosesAdapter', () => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CynthiaKamau I discovered a bug while testing the entire feature.
Issue: When you delete a diagnosis (using the form repeat controls) and launch the form again in edit mode, the deleted diagnosis still exists, meaning it was never voided. This is partly why I requested the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try retesting now
@@ -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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const saveDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); | |
const savedDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display); |
(I would also rename saveOrders
-> savedOrders
)
// handle diagnoses | ||
if (saveDiagnoses.length) { | ||
showSnackbar({ | ||
title: translateFn('diagnosisSaved', 'Diagnosis(s) saved successfully'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
title: translateFn('diagnosisSaved', 'Diagnosis(s) saved successfully'), | |
title: translateFn('diagnosisSaved', 'Diagnosis(es) saved successfully'), |
@@ -300,6 +304,40 @@ export async function hydrateRepeatField( | |||
}), | |||
); | |||
} | |||
|
|||
const unMappedDiagnoses = encounter.diagnoses.filter((diagnosis) => { | |||
return !assignedDiagnosesIds.includes(diagnosis?.diagnosis?.coded.uuid); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CynthiaKamau did you see this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, you can relook at the pr
sortedDiagnoses | ||
.filter((diagnosis) => !diagnosis.voided) | ||
.map(async (diagnosis) => { | ||
const clone = cloneRepeatField(field, diagnosis, counter++); |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Ping @samuelmale |
@brandones I left a few more comments for @CynthiaKamau |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great work @CynthiaKamau!
To make this more generic and practical, we need a way to extend the data source to load concepts from custom concept classes and, optionally, sets.
questionOptions: {
diagnosis: {
rank?: number;
isConfirmed?: boolean;
conceptClasses?: Array<string>;
conceptSet?: string;
}
}
cc: @ibacher
diagnosis: { | ||
coded: value, | ||
}, | ||
certainty: 'CONFIRMED', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should default to PROVISIONAL
but can be override-able through question options; something like: isConfirmedDiagnosis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially we were to make the feature and schema similar to what is in AFE, has that changed ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about AFE, but HFE and other standard forms in the RefApp, such as the visit note, all default to PROVISIONAL
. With the increasing need to use the form engine to replace custom-developed forms, it's important to follow standard patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will confirmed
diagnosis be handled ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the form-design perspective, the field maybe configured like:
"questionOptions": {
"diagnosis": {
"isConfirmed": true;
}
}
src/types/schema.ts
Outdated
@@ -182,6 +182,7 @@ export interface FormQuestionOptions { | |||
comment?: string; | |||
orientation?: 'vertical' | 'horizontal'; | |||
shownCommentOptions?: { validators?: Array<Record<string, any>>; hide?: { hideWhenExpression: string } }; | |||
rank?: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should encapsulate all the supported options:
diagnosis: {
rank?: number;
isConfirmed?: boolean
}
Is it supported by the backend ? I remember the question with the type problem had the same challenge |
|
The short answer is yes! Through OpenMRS' REST API, it's possible to filter concepts by class (I believe this is what the underlying datasource is doing); Then for the set, you can fetch the provided concept and retrieve the set members.
If I remember correctly, this was a pagination issue and the resolution was using a search-based mechanism as opposed to loading all concepts in a single request. |
I hope it will work for multiple concept classes |
f726e01
to
ca83cf3
Compare
@@ -0,0 +1,80 @@ | |||
import { type OpenmrsResource } from '@openmrs/esm-framework'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename the file name to singular encounter-diagnosis-adapter.ts
@@ -0,0 +1,80 @@ | |||
import { type OpenmrsResource } from '@openmrs/esm-framework'; | |||
import { type FormFieldValueAdapter, type FormProcessorContextProps } from '..'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These need to be combined with the ../types
@@ -0,0 +1,204 @@ | |||
import { type FormContextProps } from '../provider/form-provider'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this file to singular diagnosis
as well
@@ -0,0 +1,204 @@ | |||
import { type FormContextProps } from '../provider/form-provider'; | |||
import { type FormField } from '../types'; | |||
import { EncounterDiagnosesAdapter } from './encounter-diagnoses-adapter'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trickle the changes to other files as well
|
||
export let assignedDiagnosesIds: string[] = []; | ||
|
||
export const EncounterDiagnosesAdapter: FormFieldValueAdapter = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const EncounterDiagnosesAdapter: FormFieldValueAdapter = { | |
export const EncounterDiagnosisAdapter: FormFieldValueAdapter = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed to name it in plural since it can be more than one here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plural makes sense for the variables because you are actually dealing with multiple but singular makes more sense for file names and for this case the function/class. Kinda the same way we have EncounterLocationAdapter
and EncounterRoleAdapter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more similar to orders from what i understood, and its in plural, @samuelmale can also weigh in since it was part of his review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the singular version (EncounterDiagnosisAdapter
) is better because it's more atomic and seems to follow the Single Responsibility Principle. For consistency, we should rename other adapters to align with this pattern.
@@ -61,4 +62,8 @@ export const inbuiltFieldValueAdapters: RegistryItem<FormFieldValueAdapter>[] = | |||
type: 'patientIdentifier', | |||
component: PatientIdentifierAdapter, | |||
}, | |||
{ | |||
type: 'diagnosis', | |||
component: EncounterDiagnosesAdapter, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
component: EncounterDiagnosesAdapter, | |
component: EncounterDiagnosisAdapter, |
return numberA - numberB; // Sort numerically based on formFieldPath | ||
}); | ||
|
||
if (field.type === 'diagnosis') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might not specific to this PR but shouldn't we add an option to view the diagnosis in the Visit > encounters view.
How does the form look in read-only mode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me do a screenshot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might not specific to this PR but shouldn't we add an option to view the diagnosis in the Visit > encounters view. How does the form look in read-only mode?
They will be viewable there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They will be viewable there
The screen recording you shared doesn't show it. Maybe it's an old one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They will be viewable there
The screen recording you shared doesn't show it. Maybe it's an old one
Its on the video, if you check the diagnosis on the visits widget and compare it to the open form they are the same, minute 1:32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, but i have updated the video to show the diagnosis well, however, on the visits widget, voided diagnosis are also displayed
ca83cf3
to
e30f21c
Compare
e30f21c
to
2acf728
Compare
Requirements
Summary
This feature attempts to introduce encounter diagnosis in RFE
Current RFE Schema :
Proposed schema as in AFE :
Screenshots
Screen.Recording.2024-11-27.at.15.11.12.mov
Related Issue
Other