-
Notifications
You must be signed in to change notification settings - Fork 228
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-3797: Add the Encounter List Table and Tabs to Patient Chart #1987
base: main
Are you sure you want to change the base?
Conversation
Size Change: -286 kB (-1.76%) Total Size: 15.9 MB
ℹ️ View Unchanged
|
9d98eb8
to
582b365
Compare
a75476f
to
a530bbe
Compare
packages/esm-patient-chart-app/src/clinical-views/utils/helpers.ts
Outdated
Show resolved
Hide resolved
2577187
to
f02eef5
Compare
d20819d
to
e912834
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.
I haven't made it through this whole PR, but here are some of the more obvious either issues, things that weren't clear, or places where we should have improvements.
} | ||
|
||
export function formatDateTime(dateString: string): any { | ||
const parsedDate = parseDate(dateString.includes('.') ? dateString.split('.')[0] : dateString); |
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.
What is this for?
} | ||
|
||
export function obsArrayDateComparator(left, right) { | ||
return formatDateTime(right.obsDatetime) - formatDateTime(left.obsDatetime); |
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.
The output of formatDatetime()
(called at the end of formatDateTime()
is a string... so this does subtraction from two strings. s1 - s2
, where s1
and s2
are both strings is always NaN
, so this function doesn't seem to work correctly.
}); | ||
} | ||
|
||
export function formatDateTime(dateString: string): any { |
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.
Never explicitly use any
, especially when Typescript can correctly infer what's happening.
return formatDateTime(right.obsDatetime) - formatDateTime(left.obsDatetime); | ||
} | ||
|
||
export function findObs(encounter, obsConcept): Record<string, any> { |
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.
Either explicitly change any
to unknown
here or use a more correct type. Including any
in the result just basically by-passes Typescript.
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.
Likewise, encounter
and obsConcept
should be typed.
|
||
export function findObs(encounter, obsConcept): Record<string, any> { | ||
const allObs = encounter?.obs?.filter((observation) => observation.concept.uuid === obsConcept) || []; | ||
return allObs?.length == 1 ? allObs[0] : allObs?.sort(obsArrayDateComparator)[0]; |
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 exit condition would be better written:
return !allObs ? {} : allObs.length === 1 ? allObs[0] : allObs.sort(obsArrayDateComparator)[0];
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.
Note, too that this correctly ensures we are always returning a Record
. The current code could return null
or undefined
.
[formsJson, t], | ||
); | ||
|
||
const createLaunchFormAction = (encounter, 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.
This should be wrapped in useCallback()
. Also, encounter
, and mode
should be typed.
}); | ||
}) | ||
.finally(() => { | ||
mutate(); |
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.
mutate()
only needs to be called if the operation succeeded.
[onFormSave, t, mutate], | ||
); | ||
|
||
const tableRows = encounters.map((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.
Ideally, this would be a separate component (in this same file) or, at the very least, wrapped in a useMemo()
.
tableRow['actions'] = ( | ||
<OverflowMenu flipped className={styles.flippedOverflowMenu} data-testid="actions-id"> | ||
{actions.map((actionItem, index) => { | ||
const form = formsJson.length ? formsJson?.find((form) => form.name === actionItem?.form?.name) : null; |
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 form = formsJson.length ? formsJson?.find((form) => form.name === actionItem?.form?.name) : null; | |
const form = formsJson.length && actionItem?.form?.name ? formsJson.find((form) => form.name === actionItem.form.name) : null; |
); | ||
|
||
const tableRows = encounters.map((encounter) => { | ||
const tableRow: { id: string; actions: any } = { id: encounter.uuid, actions: null }; |
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.
Again, we shouldn't use an explicit any
when the type is known:
{ form: { name: string }, label: string, mode: 'view' | 'edit' | 'delete' }
87fe3eb
to
e2ffc01
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.
High-level stuff:
- There's still too much reliance on
any
types - This should all be consolidated into a single
encounter-list
folder. We don't clearly need bothclinical-views
andencounter-list
- The component in
index.ts
must usegetAsyncLifecycle()
.
import { fetchOpenMRSForms } from '../encounter-list.resource'; | ||
import { type FormSchema } from '@openmrs/esm-form-engine-lib'; | ||
|
||
export function useFormsJson(formUuids: string[]) { |
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.
It's not at all clear to me how this is meant to work. The state
we're using here is meant to store FormSchema
bojects, but to fetch them, we're loading from the /form
endpoint, the result of which are Form
objects and not FormSchema
s. I'm not sure which types are wrong, but one of them definitely is.
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 was mean to handle situations where an encounter has many forms affiliated to it, but we can start with the default use case and figure out how to do specific cases after
responses | ||
.map((response, index) => { | ||
const match = | ||
response?.data ?? response?.data?.find((result) => !result.retired && result.name === formUuids[index]); |
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.
fetchOpenMRSForms()
returns an array of Form
results, but each of those will have the Form
object as the response.data
, so response.data
is not an array of objects and so find
will always be undefined.
const match = | ||
response?.data ?? response?.data?.find((result) => !result.retired && result.name === formUuids[index]); | ||
if (!match) { | ||
console.error('Form not found: ' + formUuids[index]); |
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.
More context would be useful for this error message!
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | ||
import useSWRImmutable from 'swr'; | ||
|
||
export function usePatientDeathStatus(patientUuid: string) { |
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.
Why do we need a hook just to load the patient's death status? This is overkill. The extension will already have the patient fed into it as a prop; use that.
@@ -0,0 +1,14 @@ | |||
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | |||
import useSWRImmutable from 'swr'; |
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.
nit, but this applies to all imports from swr
in this PR.
import useSWRImmutable from 'swr'; | |
import useSWR from 'swr'; |
export interface EncounterListProps { | ||
patientUuid: string; | ||
encounterType: string; | ||
columns: Array<any>; |
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 be explicitly typed.
title?: string, | ||
encounterUuid?: string, | ||
intent: string = '*', | ||
workspaceWindowSize?: 'minimized' | 'maximized', |
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 property isn't used at all, which seems silly because it is often given a value.
form: FormSchema, | ||
action: LaunchAction = 'add', | ||
onFormSave: () => void, | ||
title?: string, |
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 property also isn't used here at all. Consider removing it altogether.
return 'No'; | ||
} else if (obs?.value?.uuid == esmPatientChartSchema.trueConceptUuid._default) { | ||
return 'Yes'; |
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.
Surely these strings need to be translatable? It would almost be better to use the actual names from the concept.
(obs?.value?.uuid != esmPatientChartSchema.trueConceptUuid._default && obs?.value?.name?.name !== 'Unknown') || | ||
obs?.value?.name?.name === 'FALSE' |
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 can't rely on names like this, as obs may not always be loaded in an en
locale.
visitTypeUuid: '', | ||
visitUuid: '', | ||
visitStartDatetime: '', | ||
visitStopDatetime: '', |
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.
For the purposes of the RefApp, we should require a visit before a form gets launched, as with the typical form-entry flow.
Ping @hadijahkyampeire , is it clear to you how to move forward with this? |
@brandones I think @CynthiaKamau or @ODORA0 will be taking this on as I am currently unavailable to work. Thanks |
@CynthiaKamau or @ODORA0 will you be taking this on? |
i will take it up |
52f397f
to
397aa4c
Compare
397aa4c
to
238ea77
Compare
Requirements
Summary
Screenshots
encounter-list-tables.mov
Related Issue
Other