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-3773 Support for Patient Program Summary in Patient Chart #1969

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

kajambiya
Copy link
Contributor

@kajambiya kajambiya commented Aug 22, 2024

Requirements

  • This PR has a title that briefly describes the work done including the ticket number. If there is a ticket, make sure your PR title includes a conventional commit label. See existing PR titles for inspiration.
  • My work conforms to the OpenMRS 3.0 Styleguide and design documentation.
  • My work includes tests or is validated by existing tests.

Summary

This PR introduces summary program summary to patient chart.

Screenshots

Screenshot 2024-09-06 at 10 03 33

Related Issue

Other

@kajambiya kajambiya force-pushed the O3-3773 branch 2 times, most recently from eaf5653 to 6b763ca Compare September 4, 2024 05:28
@kajambiya kajambiya changed the title (feat) O3-3773 Support Summary Tiles in Patient Chart (feat) O3-3773 Support for Patient Program Summary in Patient Chart Sep 4, 2024
@kajambiya kajambiya marked this pull request as ready for review September 4, 2024 06:36
@kajambiya kajambiya requested a review from samuelmale September 4, 2024 06:37
Copy link
Contributor

@hadijahkyampeire hadijahkyampeire left a comment

Choose a reason for hiding this comment

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

Mostly looks good, thanks @kajambiya , just a few comments

else return encounter[param] ? encounter[param] : '--';
}

export function formatDateTime(dateString: string): any {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use the already existing Date formatters from the framework

import { formatDate, parseDate } from '@openmrs/esm-framework';

export function getEncounterValues(encounter, param: string, isDate?: Boolean) {
if (isDate) return dayjs(encounter[param]).format('DD-MMM-YYYY');
Copy link
Member

Choose a reason for hiding this comment

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

Please use the framework rather than dayjs for formatting dates and never hard-code the date format.

@@ -112,6 +113,11 @@ export const stopVisitPatientSearchActionButton = getSyncLifecycle(stopVisitActi
moduleName,
});

export const patientProgramSummary = getSyncLifecycle(PatientSummaryOverviewList, {
Copy link
Member

Choose a reason for hiding this comment

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

This seems like it should be part of the programs app and not the patient-chart-app.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The data is related to a program but some people may classify it differently.

Copy link
Member

Choose a reason for hiding this comment

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

Right, that doesn't explain to me why it belongs here though. If it's program-specific, surely it's part of the programs app?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is not program specific, I think the name might be misleading , it’s more of a clinical view summary. I have updated the name

Copy link
Member

Choose a reason for hiding this comment

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

Part of my point is that this is a public interface intended to be consumed by multiple configurations written by implementations that may or may not understand what the original purpose of a widget is; thus naming things so that they are consistent, discoverable, and not confusing is of vital importance here.

packages/esm-patient-chart-app/src/index.ts Outdated Show resolved Hide resolved
@@ -112,6 +113,11 @@ export const stopVisitPatientSearchActionButton = getSyncLifecycle(stopVisitActi
moduleName,
});

export const patientProgramSummary = getSyncLifecycle(PatientSummaryOverviewList, {
Copy link
Member

Choose a reason for hiding this comment

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

Part of my point is that this is a public interface intended to be consumed by multiple configurations written by implementations that may or may not understand what the original purpose of a widget is; thus naming things so that they are consistent, discoverable, and not confusing is of vital importance here.


const tilesData = useMemo(
() =>
tilesDefinitions?.map((tile: any) => ({
Copy link
Member

Choose a reason for hiding this comment

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

This should be correctly typed.

Suggested change
tilesDefinitions?.map((tile: any) => ({
tilesDefinitions?.map((tile=> ({

return (
<div className={styles.tileBox}>
<div className={styles.tileBoxColumn}>
<span className={styles.tileTitle}> {column.header} </span>
Copy link
Member

Choose a reason for hiding this comment

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

Spaces here seem wrong?

'obs:(uuid,obsDatetime,voided,groupMembers,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name),' +
'names:(uuid,conceptNameType,name))),form:(uuid,name))';

export function useLastEncounter(patientUuid: string, encounterType: string) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export function useLastEncounter(patientUuid: string, encounterType: string) {
export function useMostRecentEncounter(patientUuid: string, encounterType: string) {

@CynthiaKamau CynthiaKamau force-pushed the O3-3773 branch 2 times, most recently from d5c1461 to b9b0d00 Compare October 12, 2024 07:25
Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

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

Thank you! This widget is getting much closer. A few more nit-picking comments on the code.

Now looking at the visual of this widget, we should bring it in line with the existing RefApp chart tiles specifically:

Screenshot

  • The h4 header font rendering is inconsistent with other tiles in the chart. Both the font-size (20px vs 16px) is off and the font-weight (400 vs 600).
  • The header ::after element is not correctly sized (border-bottom-width is 2px too large and padding-top is off by 1px — 2px in this component vs 3px in others)

Oddly, when located in the top-of-all-patient-dashboards-slot, as shown above the box appears too wide, but when located in the same slot as the other elements, the width is too small:

Screenshot2

}

const ClinicalViewsSummary: React.FC<OverviewListProps> = ({ patientUuid }) => {
const config = useConfig() as ConfigObject;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const config = useConfig() as ConfigObject;
const config = useConfig<ConfigObject>();

@brandones
Copy link
Contributor

Ping @ibacher

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

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

Not sure I've covered everything, but here are some things that need to be cleaned-up before this is mergeable. Goal here is to make sure that the code is in a format that the community can support over the long term. Thanks!

);
};

export const EncounterTile = React.memo(EncounterTileInternal);
Copy link
Member

Choose a reason for hiding this comment

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

Why create two separate consts for this?

export const EncounterTile = React.memo(({ patientUuid, columns, headerTitle }: EncounterTileProps) => {
   // code goes here
});

Also works.

key={encounterType}
patientUuid={patientUuid}
encounterType={encounterType}
columns={columns as FormattedCardColumn[]}
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to cast the column types here? This seems like an unnecessary escape hatch when we have full control over:

  1. The types in question
  2. The thing that's being cast to a type
  3. The function returning the thing that needs to be cast

<div className={styles.tileBox}>
{columns.map((column, ind) => (
<div key={ind} className={styles.tileBoxColumn}>
<span className={styles.tileTitle}>{t(column.title)}</span>
<span className={styles.tileValue}>--</span>
</div>
))}
</div>
Copy link
Member

Choose a reason for hiding this comment

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

So, here, an error happens and instead of reporting that to the user we just render a blank set of cells? That doesn't seem right... I'd expect us to render a message to the user explaining that the encounter data could not be loaded.

obsConcept: string,
isDate?: Boolean,
isTrueFalseConcept?: Boolean,
type?: string,
Copy link
Member

Choose a reason for hiding this comment

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

We only support a fixed set of types, so the type here should probably reflect that.

isTrueFalseConcept?: Boolean,
type?: string,
fallbackConcepts?: Array<string>,
secondaryConcept?: string,
Copy link
Member

Choose a reason for hiding this comment

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

What is a "secondaryConcept"? How does this differ from "fallbackConcepts"? These are the things that we need to explain via comments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

a secondary concept is used in a question wit the option other and a user is required to add their response in another question, which in his case is captured in the secondary concept.

Copy link
Collaborator

Choose a reason for hiding this comment

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

A fallback concept is used in instances where a tile has an alternative value, for example, enrollment date and re-enrollment date. If there is no enrollment date, then the value of the re-enrollment date can be displayed instead
cc : @eudson

Copy link
Member

Choose a reason for hiding this comment

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

The main point here was that these use-cases need to be clear in the code, so please add some comments explaining that distinction.


if (secondaryConcept && typeof obs.value === 'object' && obs.value.names) {
const primaryValue = obs.value.name.display;
if (primaryValue === t('Other non-coded')) {
Copy link
Member

Choose a reason for hiding this comment

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

This is, again, not what the translation system should be used for. Capture the UUID of the concept via the configuration system.

@@ -1,3 +1,5 @@
import { OpenmrsResource } from '@openmrs/esm-framework';
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import { OpenmrsResource } from '@openmrs/esm-framework';
import { type OpenmrsResource } from '@openmrs/esm-framework';

@CynthiaKamau CynthiaKamau force-pushed the O3-3773 branch 2 times, most recently from 8dcedbb to bd4e5dc Compare November 26, 2024 13:32
@gracepotma
Copy link
Contributor

I'm confused, is this ready for rull re-review?

@eudson
Copy link

eudson commented Dec 16, 2024

I'm confused, is this ready for rull re-review?

@gracepotma what does ready for full re-review means?

@gracepotma gracepotma requested a review from ibacher December 17, 2024 15:17
@gracepotma
Copy link
Contributor

Discussed on today's Content Package check-in call w/ Eudson, Ian, Cynthia, & Jonathan O; apparently this is ready for re-review :) We'll make this a priority to review along with the Tabbed Encounter Widget PR: #1987

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

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

At a high-level, this looks like it isn't massively distinct from #1987, at least in the backing helpers, etc. Since I'd prefer we didn't duplicate code, is it possible to rework this PR to use the same definitions from there at least for getObsFromEncounter() and similar shared functionality?

Comment on lines 18 to 21
return tileDefinitions?.map((tile: MenuCardProps) => ({
title: t(tile.tileHeader),
columns: getEncounterTileColumns(tile, t),
}));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return tileDefinitions?.map((tile: MenuCardProps) => ({
title: t(tile.tileHeader),
columns: getEncounterTileColumns(tile, t),
}));
return tileDefinitions?.map((tile: MenuCardProps) => ({
title: t(tile.tileHeader),
columns: getEncounterTileColumns(tile, t),
})) ?? [];

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

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

Couple more small points.

return <CodeSnippetSkeleton type="multi" role="progressbar" />;
}

if (error || lastEncounter == undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this just be !lastEncounter?

Comment on lines 22 to 26
async (url) => {
const cachedData = cache.get(url);
if (cachedData) {
return cachedData;
}
Copy link
Member

Choose a reason for hiding this comment

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

The manual caching here seems unnecessary. It would be simpler to use useSWRImmutable, I think.

Comment on lines 29 to 32
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const responseData = await response.json();
Copy link
Member

Choose a reason for hiding this comment

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

Pretty much everything here is redundant. openmrsFetch() already checks if response.ok and raises an error if it isn't. Also await response.json() is already what's available in the response.data field.

@CynthiaKamau
Copy link
Collaborator

I have restructured a few files that cut across this pr and the encounter list PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants