Skip to content

Commit

Permalink
(feat) - O3-3222 - ward app - add patient card element to include ris…
Browse files Browse the repository at this point in the history
…k factor obs within the current visi (#1211)

* O3-3475 - ward app - update to use latest backend bed-management module

* (feat) - O3-3222 - ward app - add patient card element to include risk factor obs within the current visit

* wording changes in schema config

* rename codedObs to codedObsTags

* styling of bed divider and row divider lines in patient card
  • Loading branch information
chibongho authored Jun 26, 2024
1 parent 881c31e commit 10951b2
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/esm-ward-app/src/beds/occupied-bed.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@

.bedDividerLine {
height: 1px;
background-color: vars.$ui-03;
background-color: vars.$ui-05;
width: 30%;
}
92 changes: 90 additions & 2 deletions packages/esm-ward-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const defaultPatientCardElementConfig: PatientCardElementConfig = {
addressFields: defaultPatientAddressFields,
},
obs: null,
codedObsTags: null,
};

export const builtInPatientCardElements: PatientCardElementType[] = [
Expand Down Expand Up @@ -57,7 +58,7 @@ export const configSchema: ConfigSchema = {
_description: 'Config for the patientCardElementType "patient-obs"',
conceptUuid: {
_type: Type.UUID,
_description: 'defines which observation value to show',
_description: 'Required. Identifies the concept to use to identify the desired observations.',
_default: null,
},
label: {
Expand Down Expand Up @@ -90,6 +91,48 @@ export const configSchema: ConfigSchema = {
_default: false,
},
},
codedObsTags: {
_description: 'Config for the patientCardElementType "patient-coded-obs-tags"',
conceptUuid: {
_type: Type.UUID,
_description: 'Required. Identifies the concept to use to identify the desired observations.',
_default: null,
},
summaryLabel: {
_type: Type.String,
_description: `Optional. The custom label or i18n key to the translated label to display for the summary tag. The summary tag shows the count of the number of answers that are present but not configured to show as their own tags. If not provided, defaults to the name of the concept.`,
_default: null,
},
summaryLabelI18nModule: {
_type: Type.String,
_description: 'Optional. The custom module to use for translation of the summary label',
_default: null,
},
summaryLabelColor: {
_type: Type.String,
_description:
'The color of the summary tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors',
_default: null,
},
tags: {
_description: `An array specifying concept sets and color. Observations with coded values that are members of the specified concept sets will be displayed as their own tags with the specified color. Any observation with coded values not belonging to any concept sets specified will be summarized as a count in the summary tag. If a concept set is listed multiple times, the first matching applied-to rule takes precedence.`,
_type: Type.Array,
_elements: {
color: {
_type: Type.String,
_description:
'Color of the tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors.',
},
appliedToConceptSets: {
_type: Type.Array,
_description: `The concept sets which the color applies to. Observations with coded values that are members of the specified concept sets will be displayed as their own tag with the specified color. If an observation's coded value belongs to multiple concept sets, the first matching applied-to rule takes precedence.`,
_elements: {
_type: Type.UUID,
},
},
},
},
},
},
},
},
Expand Down Expand Up @@ -177,7 +220,7 @@ export interface PatientAddressElementConfig {

export interface PatientObsElementConfig {
/**
* Required. Defines which observation value to show
* Required. Identifies the concept to use to identify the desired observations.
*/
conceptUuid: string;

Expand Down Expand Up @@ -208,7 +251,52 @@ export interface PatientObsElementConfig {
onlyWithinCurrentVisit?: boolean;
}

export interface PatientCodedObsTagsElementConfig {
/**
* Required. Identifies the concept to use to identify the desired observations.
*/
conceptUuid: string;

/**
* Optional. The custom label or i18n key to the translated label to display for the summary tag. The summary tag
* shows the count of the number of answers that are present but not configured to show as their own tags. If not
* provided, defaults to the name of the concept.
*/
summaryLabel?: string;
/**
* Optional. The custom module to use for translation of the summary label
*/
summaryLabelI18nModule?: string;

/**
* The color of the summary tag.
* See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors
*/
summaryLabelColor?: string;

/**
* An array specifying concept sets and color. Observations with coded values that are members of the specified concept sets
* will be displayed as their own tags with the specified color. Any observation with coded values not belonging to
* any concept sets specified will be summarized as a count in the summary tag. If a concept set is listed multiple times,
* the first matching applied-to rule takes precedence.
*/
tags: Array<{
/**
* Color of the tag. See https://react.carbondesignsystem.com/?path=/docs/components-tag--overview for a list of supported colors.
*/
color: string;

/**
* The concept sets which the color applies to. Observations with coded values that are members of the specified concept sets
* will be displayed as their own tag with the specified color.
* If an observation's coded value belongs to multiple concept sets, the first matching applied-to rule takes precedence.
*/
appliedToConceptSets: Array<string>;
}>;
}

export type PatientCardElementConfig = {
address: PatientAddressElementConfig;
obs: PatientObsElementConfig;
codedObsTags: PatientCodedObsTagsElementConfig;
};
11 changes: 11 additions & 0 deletions packages/esm-ward-app/src/hooks/useConcept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Concept, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
import useSWRImmutable from 'swr/immutable';

export function useConcepts(uuids: string[], rep = 'default') {
const apiUrl = `${restBaseUrl}/concept?references=${uuids.join()}&v=${rep}`;
const { data, ...rest } = useSWRImmutable<{ data: { results: Array<Concept> } }, Error>(apiUrl, openmrsFetch);
return {
concepts: data?.data?.results,
...rest,
};
}
1 change: 1 addition & 0 deletions packages/esm-ward-app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const patientCardElementTypes = [
'patient-age',
'patient-address',
'patient-obs',
'patient-coded-obs-tags',
'admission-time',
] as const;
export type PatientCardElementType = (typeof patientCardElementTypes)[number];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { SkeletonText, Tag } from '@carbon/react';
import { translateFrom, type OpenmrsResource } from '@openmrs/esm-framework';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { type PatientCodedObsTagsElementConfig } from '../../config-schema';
import { moduleName } from '../../constant';
import { useObs } from '../../hooks/useObs';
import { type WardPatientCardElement } from '../../types';
import styles from '../ward-patient-card.scss';
import { obsCustomRepresentation, useConceptToTagColorMap } from './ward-patient-obs.resource';

/**
* The WardPatientCodedObsTags displays observations of coded values of a particular concept in the active visit as tags.
* Typically, these are taken from checkbox fields from a form. Each answer value can either be configured
* to show as its own tag, or collapsed into a summary tag show the number of these values present.
*
* This is a rather specialized element;
* for a more general display of obs value, use WardPatientObs instead.
* @param config
* @returns
*/
const wardPatientCodedObsTags = (config: PatientCodedObsTagsElementConfig) => {
const WardPatientCodedObsTags: WardPatientCardElement = ({ patient, visit }) => {
const { conceptUuid, summaryLabel, summaryLabelColor, summaryLabelI18nModule } = config;
const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation);
const { t } = useTranslation();
const { data: conceptToTagColorMap } = useConceptToTagColorMap(config);

if (isLoading) {
return <SkeletonText />;
} else {
const obsToDisplay = data?.data?.results?.filter((o) => {
const matchVisit = o.encounter.visit?.uuid == visit?.uuid;
return matchVisit || visit == null; // TODO: remove visit == null hack when server API supports returning visit
});

const summaryLabelToDisplay =
summaryLabel != null
? translateFrom(summaryLabelI18nModule ?? moduleName, summaryLabel)
: obsToDisplay?.[0]?.concept?.display;

const obsNodes = obsToDisplay?.map((o) => {
const { display, uuid } = o.value as OpenmrsResource;

const color = conceptToTagColorMap?.get(uuid);
if (color) {
return (
<Tag type={color} key={uuid}>
{display}
</Tag>
);
} else {
return null;
}
});

const obsWithNoTagCount = obsNodes.filter((o) => o == null).length;
if (obsNodes?.length > 0 || obsWithNoTagCount > 0) {
return (
<div>
<span className={styles.wardPatientObsLabel}>
{obsNodes}
{obsWithNoTagCount > 0 ? (
<Tag type={summaryLabelColor}>
{t('countItems', '{{count}} {{item}}', { count: obsWithNoTagCount, item: summaryLabelToDisplay })}
</Tag>
) : null}
</span>
</div>
);
} else {
return null;
}
}
};

return WardPatientCodedObsTags;
};

export default wardPatientCodedObsTags;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { openmrsFetch, restBaseUrl, type Concept } from '@openmrs/esm-framework';
import useSWRImmutable from 'swr/immutable';
import { type PatientCodedObsTagsElementConfig } from '../../config-schema';

// prettier-ignore
export const obsCustomRepresentation =
'custom:(uuid,display,obsDatetime,value,' +
'concept:(uuid,display),' +
'encounter:(uuid,display,' +
'visit:(uuid,display)))';

// get the setMembers of a concept set
const conceptSetCustomRepresentation = 'custom:(uuid,setMembers:(uuid))';

export function useConceptToTagColorMap(codedObsTagsConfig: PatientCodedObsTagsElementConfig) {
// fetch the members of the concept sets and process the data
// to return conceptToTagColorMap (wrapped in a promise).
// Let swr cache the result of this function.
const fetchAndMap = (url: string) => {
const conceptSetToTagColorMap = new Map<string, string>();
for (const tag of codedObsTagsConfig.tags) {
const { color, appliedToConceptSets } = tag;
for (const answer of appliedToConceptSets ?? []) {
if (!conceptSetToTagColorMap.has(answer)) {
conceptSetToTagColorMap.set(answer, color);
}
}
}

return openmrsFetch<{ results: Array<Concept> }>(url).then((data) => {
const conceptSets = data.data.results;
const conceptToTagColorMap = new Map<string, string>();
if (conceptSets) {
for (const conceptSet of conceptSets) {
for (const concept of conceptSet.setMembers) {
if (!conceptToTagColorMap.has(concept.uuid)) {
conceptToTagColorMap.set(concept.uuid, conceptSetToTagColorMap.get(conceptSet.uuid));
}
}
}
}

return conceptToTagColorMap;
});
};

const conceptSetUuids = codedObsTagsConfig.tags.flatMap((tag) => tag.appliedToConceptSets);
const apiUrl = `${restBaseUrl}/concept?references=${conceptSetUuids.join()}&v=${conceptSetCustomRepresentation}`;
const conceptToTagColorMap = useSWRImmutable(apiUrl, fetchAndMap);

return conceptToTagColorMap;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ import { useObs } from '../../hooks/useObs';
import { type WardPatientCardElement } from '../../types';
import styles from '../ward-patient-card.scss';
import { moduleName } from '../../constant';

// prettier-ignore
const obsCustomRepresentation =
'custom:(uuid,display,obsDatetime,value,' +
'concept:(uuid,display),' +
'encounter:(uuid,display,' +
'visit:(uuid,display)))';
import { obsCustomRepresentation } from './ward-patient-obs.resource';

const wardPatientObs = (config: PatientObsElementConfig) => {
const WardPatientObs: WardPatientCardElement = ({ patient, visit }) => {
Expand All @@ -40,18 +34,21 @@ const wardPatientObs = (config: PatientObsElementConfig) => {
const obsNodes = obsToDisplay?.map((o) => {
const { value } = o;
const display: any = (value as OpenmrsResource)?.display ?? o.value;

return <span> {display} </span>;
return <span key={o.uuid}> {display} </span>;
});

return (
<div>
<span className={styles.wardPatientObsLabel}>
{labelToDisplay ? t('labelColon', '{{label}}:', { label: labelToDisplay }) : ''}
</span>
{obsNodes}
</div>
);
if (obsNodes?.length > 0) {
return (
<div>
<span className={styles.wardPatientObsLabel}>
{labelToDisplay ? t('labelColon', '{{label}}:', { label: labelToDisplay }) : ''}
</span>
{obsNodes}
</div>
);
} else {
return null;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import WardPatientName from './row-elements/ward-patient-name';
import React from 'react';
import styles from './ward-patient-card.scss';
import wardPatientObs from './row-elements/ward-patient-obs';
import wardPatientCodedObsTags from './row-elements/ward-patient-coded-obs-tags';

export function usePatientCardRows(location: string) {
const { wardPatientCards } = useConfig<WardConfigObject>();
Expand Down Expand Up @@ -84,5 +85,8 @@ function getPatientCardElementFromDefinition(
case 'patient-obs': {
return wardPatientObs(config.obs);
}
case 'patient-coded-obs-tags': {
return wardPatientCodedObsTags(config.codedObsTags);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
padding: spacing.$spacing-04;
}

.wardPatientCardRow:empty {
display: none;
}

.wardPatientCardHeader {
@extend .dotSeparatedChildren;
display: flex;
Expand Down

0 comments on commit 10951b2

Please sign in to comment.