diff --git a/packages/esm-patient-chart-app/src/clinical-views/components/clinical-views-summary.component.tsx b/packages/esm-patient-chart-app/src/clinical-views/components/clinical-views-summary.component.tsx index 72a4fb04aa..bf8758e1dc 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/components/clinical-views-summary.component.tsx +++ b/packages/esm-patient-chart-app/src/clinical-views/components/clinical-views-summary.component.tsx @@ -1,15 +1,15 @@ -import React, { useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { EncounterTile } from './encounter-tile/encounter-tile.component'; import { type ConfigObject, useConfig } from '@openmrs/esm-framework'; -import { getEncounterTileColumns, type MenuCardProps } from '../utils/encounter-tile-config-builder'; +import { getEncounterTileColumns, type MenuCardProps } from '../utils'; interface OverviewListProps { patientUuid: string; } -const ClinicalViewsSummary: React.FC = ({ patientUuid }) => { +const ClinicalViewsSummary: React.FC = memo(({ patientUuid }) => { const config = useConfig(); const { t } = useTranslation(); const tileDefinitions = config.tilesDefinitions; @@ -29,6 +29,6 @@ const ClinicalViewsSummary: React.FC = ({ patientUuid }) => { ))} ); -}; +}); export default ClinicalViewsSummary; diff --git a/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/encounter-tile.component.tsx b/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/encounter-tile.component.tsx index 6192cc83c4..e5356a81cc 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/encounter-tile.component.tsx +++ b/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/encounter-tile.component.tsx @@ -1,17 +1,18 @@ -import { CodeSnippetSkeleton, Tile, Column, Layer } from '@carbon/react'; import React, { useMemo } from 'react'; +import { useLayoutType } from '@openmrs/esm-framework'; +import { CodeSnippetSkeleton, Tile, Column, Layer } from '@carbon/react'; import styles from './tile.scss'; import { groupColumnsByEncounterType } from '../../utils/helpers'; import { useLastEncounter } from '../../hooks/useLastEncounter'; import { LazyCell } from '../../../lazy-cell/lazy-cell.component'; import { useTranslation } from 'react-i18next'; -import { type FormattedCardColumn } from '../../utils/encounter-tile-config-builder'; -import { useLayoutType } from '@openmrs/esm-framework'; export interface EncounterTileColumn { key: string; header: string; encounterUuid: string; + concept: string; + title?: string; getObsValue: (encounter: any) => string | Promise; getSummaryObsValue?: (encounter: any) => string | Promise; encounter?: any; @@ -28,7 +29,7 @@ export interface EncounterValuesTileProps { column: any; } -const EncounterTileInternal: React.FC = ({ patientUuid, columns, headerTitle }) => { +export const EncounterTile = React.memo(({ patientUuid, columns, headerTitle }: EncounterTileProps) => { const columnsByEncounterType = useMemo(() => groupColumnsByEncounterType(columns), [columns]); const isTablet = useLayoutType() === 'tablet'; @@ -39,41 +40,39 @@ const EncounterTileInternal: React.FC = ({ patientUuid, colu

{headerTitle}

- {Object.entries(columnsByEncounterType).map(([encounterType, columns]) => ( + {Object.entries(columnsByEncounterType).map(([encounterUuid, columns]) => ( ))} ); -}; - -export const EncounterTile = React.memo(EncounterTileInternal); +}); const EncounterData: React.FC<{ patientUuid: string; encounterType: string; - columns: FormattedCardColumn[]; + columns: EncounterTileColumn[]; }> = ({ patientUuid, encounterType, columns }) => { const { lastEncounter, isLoading, error, isValidating } = useLastEncounter(patientUuid, encounterType); const { t } = useTranslation(); if (isLoading || isValidating) { - return ; + return ; } - if (error || lastEncounter === undefined) { + if (error || lastEncounter == undefined) { return (
{columns.map((column, ind) => ( -
+
{t(column.title)} - -- + {error?.message}
))}
@@ -83,15 +82,20 @@ const EncounterData: React.FC<{ return (
{columns.map((column, ind) => ( -
- {column.title} +
+ {column.header} {column.hasSummary && ( - - - + <> + + + + + + + )}
))} diff --git a/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/tile.scss b/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/tile.scss index 22ed4c16cf..4fdaff7fac 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/tile.scss +++ b/packages/esm-patient-chart-app/src/clinical-views/components/encounter-tile/tile.scss @@ -15,37 +15,29 @@ } .tileTitle { - @include type.type-style('label-01'); + @include type.type-style('body-long-01'); display: block; padding-top: layout.$spacing-05; } .columnContainer { - margin: 0px 0px layout.$spacing-06 layout.$spacing-06; display: flex; flex-direction: row; } -.tileBoxColumn { - width: 100%; - display: 'flex'; - flex-direction: 'row'; -} - .tileSubTitle { @include type.type-style('body-compact-01'); } .tileValue { - @include type.type-style('body-long-01'); - margin: layout.$spacing-05 layout.$spacing-10 layout.$spacing-06 0; + @include type.type-style('label-01'); + margin: layout.$spacing-04 0 layout.$spacing-04; + display: block; } .tileBox { - width: 25%; display: flex; - flex-direction: row; - flex: 1; + gap: layout.$spacing-04; } .desktopHeading { @@ -72,7 +64,7 @@ content: ''; display: block; width: layout.$spacing-07; - padding-top: 0.188rem; + padding-top: layout.$spacing-01; border-bottom: 0.375rem solid var(--brand-03); } } @@ -81,7 +73,7 @@ content: ''; display: block; width: layout.$spacing-07; - padding-top: 0.188rem; + padding-top: layout.$spacing-01; border-bottom: 0.375rem solid var(--brand-03); } diff --git a/packages/esm-patient-chart-app/src/clinical-views/hooks/useLastEncounter.ts b/packages/esm-patient-chart-app/src/clinical-views/hooks/useLastEncounter.ts index 5891c62679..fa68978d4a 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/hooks/useLastEncounter.ts +++ b/packages/esm-patient-chart-app/src/clinical-views/hooks/useLastEncounter.ts @@ -9,19 +9,41 @@ export const encounterRepresentation = 'obs:(uuid,obsDatetime,voided,groupMembers,concept:(uuid,display,name:(uuid,name)),value:(uuid,name:(uuid,name,display),' + 'names:(uuid,conceptNameType,name,display))),form:(uuid,name))'; +const cache = new Map(); + export function useLastEncounter(patientUuid: string, encounterType: string) { const query = `encounterType=${encounterType}&patient=${patientUuid}&limit=1&order=desc&startIndex=0`; - const endpointUrl = `/ws/rest/v1/encounter?${query}&v=${encounterRepresentation}`; + const endpointUrl = + patientUuid && encounterType ? `/ws/rest/v1/encounter?${query}&v=${encounterRepresentation}` : null; + + const cacheKey = endpointUrl; - const { data, error, isValidating } = useSWR<{ data: { results: Array } }, Error>( - endpointUrl, - openmrsFetch, - { dedupingInterval: 5000, refreshInterval: 0 }, + const { data, error, isValidating, isLoading } = useSWR<{ results: Array }, Error>( + cacheKey, + async (url) => { + const cachedData = cache.get(url); + if (cachedData) { + return cachedData; + } + const response = await openmrsFetch(url); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const responseData = await response.json(); + cache.set(url, responseData); + return responseData; + }, + { + dedupingInterval: 50000, + refreshInterval: 0, + }, ); + return { - lastEncounter: data ? data?.data?.results.shift() : null, + lastEncounter: data ? data.results?.length > 0 && data.results[0] : null, error, - isLoading: !data && !error, + isLoading, isValidating, }; } diff --git a/packages/esm-patient-chart-app/src/clinical-views/utils/helpers.ts b/packages/esm-patient-chart-app/src/clinical-views/utils/helpers.ts index 1d6b2871af..f0b550ba42 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/utils/helpers.ts +++ b/packages/esm-patient-chart-app/src/clinical-views/utils/helpers.ts @@ -1,6 +1,6 @@ import { age, formatDate, type OpenmrsResource, parseDate } from '@openmrs/esm-framework'; import { type TFunction } from 'i18next'; -import { esmPatientChartSchema } from '../../config-schema'; +import { type EncounterTileColumn } from '../components/encounter-tile/encounter-tile.component'; export interface Observation { uuid: string; @@ -41,9 +41,16 @@ export interface Encounter extends OpenmrsResource { visit?: string; } +export enum EncounterPropertyType { + location = 'location', + provider = 'provider', + visitType = 'visitType', + ageAtEncounter = 'ageAtEncounter', +} + export function getEncounterValues(encounter: Encounter, param: string, isDate?: Boolean) { if (isDate) return formatDate(encounter[param]); - else return encounter[param] ? encounter[param] : '--'; + else return encounter[param] ?? '--'; } export function obsArrayDateComparator(left: Observation, right: Observation) { @@ -52,7 +59,7 @@ export function obsArrayDateComparator(left: Observation, right: Observation) { export function findObs(encounter: Encounter, obsConcept: string): Observation { const allObs = encounter?.obs?.filter((observation) => observation.concept.uuid === obsConcept) || []; - return allObs?.length == 1 ? allObs[0] : allObs?.sort(obsArrayDateComparator)[0]; + return allObs?.length == 1 ? allObs[0] : allObs.sort(obsArrayDateComparator)[0]; } export function getObsFromEncounters(encounters: Encounter, obsConcept: string) { @@ -106,7 +113,7 @@ export function getObsFromEncounter( obsConcept: string, isDate?: Boolean, isTrueFalseConcept?: Boolean, - type?: string, + type?: EncounterPropertyType, fallbackConcepts?: Array, secondaryConcept?: string, t?: TFunction, @@ -118,33 +125,12 @@ export function getObsFromEncounter( if (isTrueFalseConcept) { if (typeof obs?.value === 'object') { - if ( - (obs?.value?.uuid != esmPatientChartSchema.trueConceptUuid._default && obs?.value?.name?.name !== 'Unknown') || - obs?.value?.name?.name === t('FALSE') - ) { - return t('No'); - } else if (obs?.value?.uuid == esmPatientChartSchema.trueConceptUuid._default) { - return t('Yes'); - } else { - return obs?.value?.name?.name; - } + return obs?.value?.name?.name; } } - if (type === 'location') { - return encounter.location.display; - } - - if (type === 'provider') { - return encounter.encounterProviders.map((p) => p.provider.name).join(' | '); - } - - if (type === 'visitType') { - return encounter.encounterType.name; - } - - if (type === 'ageAtHivTest') { - return age(encounter.patient.birthDate, encounter.encounterDatetime); + if (type) { + getEncounterProperty(encounter, type); } if (secondaryConcept && typeof obs.value === 'object' && obs.value.names) { @@ -178,12 +164,30 @@ export function getObsFromEncounter( return obs.value; } -export const groupColumnsByEncounterType = (columns) => { - return columns.reduce((acc, column) => { - if (!acc[column.encounterType]) { - acc[column.encounterType] = []; +export const groupColumnsByEncounterType = (columns: EncounterTileColumn[]): Record => { + return columns.reduce((acc: Record, column) => { + if (!acc[column.encounterUuid]) { + acc[column.encounterUuid] = []; } - acc[column.encounterType].push(column); + acc[column.encounterUuid].push(column); return acc; }, {}); }; + +export const getEncounterProperty = (encounter: Encounter, type: EncounterPropertyType) => { + if (type === 'location') { + return encounter.location.display; + } + + if (type === 'provider') { + return encounter.encounterProviders.map((p) => p.provider.name).join(' | '); + } + + if (type === 'visitType') { + return encounter.encounterType.name; + } + + if (type === 'ageAtEncounter') { + return age(encounter.patient.birthDate, encounter.encounterDatetime); + } +}; diff --git a/packages/esm-patient-chart-app/src/clinical-views/utils/encounter-tile-config-builder.ts b/packages/esm-patient-chart-app/src/clinical-views/utils/index.ts similarity index 85% rename from packages/esm-patient-chart-app/src/clinical-views/utils/encounter-tile-config-builder.ts rename to packages/esm-patient-chart-app/src/clinical-views/utils/index.ts index 2338c0ff5a..e19f285be5 100644 --- a/packages/esm-patient-chart-app/src/clinical-views/utils/encounter-tile-config-builder.ts +++ b/packages/esm-patient-chart-app/src/clinical-views/utils/index.ts @@ -1,5 +1,6 @@ import { type TFunction } from 'i18next'; -import { getConceptFromMappings, getObsFromEncounter } from './helpers'; +import { type EncounterPropertyType, getConceptFromMappings, getObsFromEncounter } from './helpers'; +import { type EncounterTileColumn } from '../components/encounter-tile/encounter-tile.component'; export interface MenuCardProps { tileHeader: string; @@ -23,7 +24,7 @@ export interface ColumnDefinition { conceptMappings?: Array; summaryConcept?: SummaryConcept; isTrueFalseConcept?: boolean; - type?: string; + type?: EncounterPropertyType; fallbackConcepts?: Array; } @@ -45,7 +46,7 @@ const calculateDateDifferenceInDate = (givenDate: string): string => { }; export const getEncounterTileColumns = (tileDefinition: MenuCardProps, t?: TFunction) => { - const columns: Array = tileDefinition.columns?.map((column: ColumnDefinition) => ({ + const columns: Array = tileDefinition.columns?.map((column: ColumnDefinition) => ({ key: column.title, header: t(column.title), concept: column.concept, @@ -72,14 +73,14 @@ export const getEncounterTileColumns = (tileDefinition: MenuCardProps, t?: TFunc ? (encounter) => { let summaryValue; - if (column.summaryConcept.secondaryConcept) { + if (column.summaryConcept?.secondaryConcept) { const primaryConceptType = getObsFromEncounter(encounter, column.summaryConcept.primaryConcept); if (primaryConceptType !== '--') { summaryValue = primaryConceptType; } else { summaryValue = getObsFromEncounter(encounter, column.summaryConcept.secondaryConcept); } - } else if (column.summaryConcept.hasCalculatedDate) { + } else if (column.summaryConcept?.hasCalculatedDate) { const primaryDate = getObsFromEncounter( encounter, column.summaryConcept.primaryConcept, @@ -94,8 +95,8 @@ export const getEncounterTileColumns = (tileDefinition: MenuCardProps, t?: TFunc } else { summaryValue = getObsFromEncounter( encounter, - column.summaryConcept.primaryConcept, - column.summaryConcept.isDate, + column.summaryConcept?.primaryConcept, + column.summaryConcept?.isDate, ); } return typeof summaryValue === 'string' ? summaryValue : summaryValue?.name?.name || '--'; diff --git a/packages/esm-patient-chart-app/src/config-schema.ts b/packages/esm-patient-chart-app/src/config-schema.ts index 3caa538a02..b6d2ae347c 100644 --- a/packages/esm-patient-chart-app/src/config-schema.ts +++ b/packages/esm-patient-chart-app/src/config-schema.ts @@ -140,7 +140,7 @@ export const esmPatientChartSchema = { }, trueConceptUuid: { _type: Type.String, - _description: 'Default concept uuid for true in forms', + _description: 'The UUID of the concept for true', _default: 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3', }, }; diff --git a/packages/esm-patient-chart-app/src/routes.json b/packages/esm-patient-chart-app/src/routes.json index 331f7d2274..2ace2d77c0 100644 --- a/packages/esm-patient-chart-app/src/routes.json +++ b/packages/esm-patient-chart-app/src/routes.json @@ -33,6 +33,8 @@ { "name": "clinical-views-summary", "component": "clinicalViewsSummary", + "slot": "patient-chart-encounters-dashboard-slot", + "order": 0, "online": true, "offline": true }, diff --git a/packages/esm-patient-common-lib/src/types/index.ts b/packages/esm-patient-common-lib/src/types/index.ts index 9d6270e1e0..9907db70ca 100644 --- a/packages/esm-patient-common-lib/src/types/index.ts +++ b/packages/esm-patient-common-lib/src/types/index.ts @@ -1,4 +1,4 @@ -import { OpenmrsResource } from '@openmrs/esm-framework'; +import { type OpenmrsResource } from '@openmrs/esm-framework'; export * from './test-results';