Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SkeletonIcon } from '@carbon/react';
import React from 'react';
import styles from './empty-bed.scss';
import { SkeletonIcon, SkeletonText } from '@carbon/react';

const EmptyBedSkeleton = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './empty-bed.scss';
import admittedPatientHeaderStyles from '../admitted-patient/admitted-patient-header.scss';
import wardPatientCardStyles from '../ward-patient-card/ward-patient-card.scss';
import { type Bed } from '../types';

interface EmptyBedProps {
Expand All @@ -10,7 +10,9 @@ interface EmptyBedProps {
const EmptyBed: React.FC<EmptyBedProps> = ({ bed }) => {
return (
<div className={styles.container}>
<span className={admittedPatientHeaderStyles.bedNumber}>{bed.bedNumber}</span>
<span className={`${wardPatientCardStyles.wardPatientBedNumber} ${wardPatientCardStyles.empty}`}>
{bed.bedNumber}
</span>
<p className={styles.emptyBed}>Empty Bed</p>
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be translated.

</div>
);
Expand Down
40 changes: 40 additions & 0 deletions packages/esm-ward-app/src/beds/occupied-bed.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { type Patient } from '@openmrs/esm-framework';
import { type Bed } from '../types';
import styles from './occupied-bed.scss';
import { Tag } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import WardPatientCard from '../ward-patient-card/ward-pateint-card.component';
Copy link
Member

Choose a reason for hiding this comment

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

ward-pateint-card.component is misspelled


export interface OccupiedBedProps {
patients: Patient[];
bed: Bed;
}
const OccupiedBed: React.FC<OccupiedBedProps> = ({ patients, bed }) => {
return (
<div className={styles.occupiedBed}>
{patients.map((patient, index: number) => {
const last = index === patients.length - 1;
return (
<div key={patient.uuid + ' ' + index}>
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 patient.uuid be unique?

Copy link
Contributor

Choose a reason for hiding this comment

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

However you should append a namespace for the key. We don't want random components around the app all having their key equal the patient UUID.

Suggested change
<div key={patient.uuid + ' ' + index}>
<div key={`occupied-bed-pt-${patient.uuid}`}>

<WardPatientCard patient={patient} bed={bed} status={'admitted'} />
Copy link
Member

Choose a reason for hiding this comment

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

An occupied bed != admitted, it just means the bed is occupied. Admission is a separate, orthongonal concern.

{!last && <BedShareDivider />}
</div>
);
})}
</div>
);
};

const BedShareDivider = () => {
const { t } = useTranslation();
return (
<div className={styles.bedDivider}>
<div className={styles.bedDividerLine}></div>
<Tag>{t('bedshare', 'Bed share')}</Tag>
<div className={styles.bedDividerLine}></div>
</div>
);
};

export default OccupiedBed;
24 changes: 24 additions & 0 deletions packages/esm-ward-app/src/beds/occupied-bed.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@import '~@openmrs/esm-styleguide/src/vars';
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 '~@openmrs/esm-styleguide/src/vars';
@use '@openmrs/esm-styleguide/src/vars';

Requires vars.$ui-02, etc., but slightly friendlier to the Sass compiler.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know about this! Does it compile faster, or result in better output, or both?

@denniskigen should we add something about this to the docs, if we have docs on SCSS/style?

Copy link
Member

Choose a reason for hiding this comment

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

Theoretically, it compiles faster and produces more predictable output. However, the strongest reason is that the Sass team has said they're planning on deprecating @import altogether. See here for more. (I say theoretically, because the vars file itself doesn't really add any bloat, but @import parts of Carbon definitely would).


.occupiedBed {
display: flex;
flex-direction: column;
background-color: $ui-02;
}

.bedDivider {
background-color: $ui-02;
color: $text-02;
padding: spacing.$spacing-01;
display: flex;
align-items: center;
justify-content: space-between;
}

.bedDividerLine {
height: 1px;
background-color: $ui-03;
width: 30%;
}
45 changes: 45 additions & 0 deletions packages/esm-ward-app/src/beds/occupied-bed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import OccupiedBed from './occupied-bed.component';
import React from 'react';
import { mockAdmissionLocation } from '../../../../__mocks__/wards.mock';
import { bedLayoutToBed, filterBeds } from '../ward-view/ward-view.resource';
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
import { configSchema, defaultBentoElementConfig } from '../config-schema';

const defaultConfigSchema = getDefaultsFromConfigSchema(configSchema);

jest.mocked(useConfig).mockReturnValue({
...defaultConfigSchema,
});

const mockBedLayouts = filterBeds(mockAdmissionLocation);

const mockBedToUse = mockBedLayouts[0];
jest.replaceProperty(mockBedToUse.patient.person, 'preferredName', {
uuid: '',
givenName: 'Alice',
familyName: 'Johnson',
});
const mockPatient = mockBedToUse.patient;
const mockBed = bedLayoutToBed(mockBedToUse);

describe('Occupied bed: ', () => {
it('renders a single bed with patient details', () => {
render(<OccupiedBed patients={[mockPatient]} bed={mockBed} />);
const patientName = screen.getByText('Alice Johnson');
expect(patientName).toBeInTheDocument();
const patientAge = `${mockPatient.person.age} yrs`;
expect(screen.getByText(patientAge)).toBeInTheDocument();
const defaultAddressFields = defaultBentoElementConfig.addressFields;
defaultAddressFields.forEach((addressField) => {
const addressFieldValue = mockPatient.person.preferredAddress[addressField] as string;
expect(screen.getByText(addressFieldValue)).toBeInTheDocument();
});
});

it('renders a divider for shared patients', () => {
render(<OccupiedBed patients={[mockPatient, mockPatient]} bed={mockBed} />);
const bedShareText = screen.getByTitle('Bed share');
expect(bedShareText).toBeInTheDocument();
});
});
121 changes: 118 additions & 3 deletions packages/esm-ward-app/src/config-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,120 @@
import { type ConfigSchema } from '@openmrs/esm-framework';
import { type PersonAddress, Type, validator, validators, type ConfigSchema } from '@openmrs/esm-framework';
import { type BentoElementType, bentoElementTypes } from './types';

export const configSchema: ConfigSchema = {};
const defaultWardPatientCard: WardPatientCardDefinition = {
card: {
id: 'default-card',
header: ['bed-number', 'patient-name', 'patient-age', 'patient-address'],
},
appliedTo: null,
};

export interface ConfigObject {}
export const defaultBentoElementConfig: BentoElementConfig = {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why this is and why it has addressFields in it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's supposed to be a common config object for various types of bentoElements. (See comment below)

addressFields: ['cityVillage', 'country'],
};

export const builtInBentoElements: BentoElementType[] = [
'bed-number',
'patient-name',
'patient-age',
'patient-address',
'admission-time',
];

export const configSchema: ConfigSchema = {
wardPatientCards: {
_description: 'Configure the display of ward patient cards',
bentoElementDefinitions: {
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 bentoElementDefinition? I'm not entirely sure this nomenclature is clear.

Copy link
Member

Choose a reason for hiding this comment

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

I think something like patientCardElement might be clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's the individual elements that can show up within a patient card row (like bed number, patient name, patiant age, patient address). I'll change it to PatientCardRowElement.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see where you're coming from with PatientCardRowElement @chibongho but unfortunately I think the formal logic of the name does not align with intuition—I think it is very easy to read PatientCardRowElement and think the element is supposed to be a row. I think @mseaton 's suggestion of patientCardElement is clearest.

Copy link
Contributor

Choose a reason for hiding this comment

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

So this would be patientCardElementDefinitions

_type: Type.Array,
_default: [],
_elements: {
id: {
_type: Type.String,
_description: 'The unique identifier for this custom bento element',
},
elementType: {
_type: Type.String,
_description: 'The bento element type',
_validators: [validators.oneOf(bentoElementTypes)],
},
config: {
addressFields: {
Copy link
Member

Choose a reason for hiding this comment

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

I really don't like this here. A specific type of patientCardElement should have a configuration that takes in an address field. Just like other types of patientCardElement (like Gravity) might take in a Concept. This should not be baked into a generic config schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A similar change was made to the queues app. I believe it was to facilitate adding validators on the fields. I'm not sure if it's possible to have a validator work by defining specific configs per element type, but I can play around with it.
@brandones what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

@mseaton This is the way we handle this kind of thing in registration and service queues; it's the best pattern we've figured out for this.

@chibongho You will definitely want to add a _default value for addressFields so that it is optional, since indeed it would be very bad if we required implementers to configure addressFields for an element of type bed-number. We want implementers to be able to provide config like

{
  "patientCardElementDefinitions": [
    { "id": "bednum", "elementType": "bed-number" },
    { "id": "addr", "elementType": "patient-address", "config": { "addressFields": ["cityVillage"] } } 
  ]
}

and then we can add validators that ensure that implementers don't provide an element config that doesn't make sense with the element type.

@mseaton The only way I can imagine supporting what you're talking about would be to have a separate definitions array for each type:

{
  "patientCardBedNumberElementDefinitions": [
    { "id": "bednum" }
  ],
  "patientCardPatientAddressElementDefinitions": [
    { "id": "addr", "addressFields": ["cityVillage"] }
  ]
}

So that would be our other option here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Given the frequency with which this pattern appears, I'm definitely open to thinking about whether we can add a schema feature that supports it better. It would be really nice if the config system had some way of knowing what element config schema should be used based on the element type.

Copy link
Member

Choose a reason for hiding this comment

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

Quick first pass. This usually comes up in the context of arrays of elements right? Maybe for arrays we can support something like the following:

someKey: {
  _type: Type.Array,
  _default: [],
  _key: {
    id: {
      _type: Type.String,
      _description: 'The config-internal identifier for this element',
    },
  }
  _elements: [{
    id: 'address',  // applies to element whose key matches this
    elementType: {
      _type: Type.String,
      _description: 'The bento element type',
      _validators: [validators.oneOf(bentoElementTypes)],
    },
    config: {
        _type: Type.Array,
        _description: 'For bentoElementType "patient-address", defining which address fields to show',
    }
  }, {
    elementType: {
      _type: Type.String,
      _description: 'The bento element type',
      _validators: [validators.oneOf(bentoElementTypes)],
    },
  }]
}

That could be typed like:

type ConfigElement = {
  id: string;
  elementType: string;
}

type AddressConfigElement = ConfigElement & {
  id: 'address';
  config: Array<string>;
}

someKey: Array<ConfigElement>;

_type: Type.Array,
_description: 'For bentoElementType "patient-address", defining which address fields to show',
},
},
},
},
cardDefinitions: {
_type: Type.Array,
_default: [defaultWardPatientCard],
_description: `An array of card configuration. A card configuration can be applied to different ward locations.
If multiple card configurations apply to a location, only the first one is chosen.`,
_elements: {
id: {
_type: Type.String,
_description: 'The unique identifier for this card definition. Currently unused, but that might change.',
},
card: {
header: {
Copy link
Member

Choose a reason for hiding this comment

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

Per our discussion, I'm still of the belief that a card should be made up of 1-N sections, which contain 1-N elements. And these sections could have a type associated with them, or otherwise be configured to indicate if they should appear up in particular views (eg. in a minimal view that is used in the Admission Request workspace, or in a maximal view used in the ward cards.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think your suggestion of having every Patient Card Rows be modeled as a "bento box", even with rows that don't fit neatly within that paradigm as a bento box with one element (possibly an extension), is worth exploring. I've tried modeling this by having different row types and wasn't that satisfied with how complex it was.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mseaton Yeah @chibongho and I had a nice discussion about this yesterday and arrived at this design. The card has a header row, with name, age, etc, and a bunch of define-in-config elements. It also has a footer row that just has an array of define-in-config elements. The rows in the middle are I think a very good use of the extension system. Using the extension system for this has the following advantages:

  • Simplifies the card config schema. Having rows as define-in-config elements that each contain their own define-in-config elements would make this far more complex than any existing schema. Even in the registration app, which has define-in-config sections containing define-in-config fields, there are not multiple types of sections with different valid types of configuration, as would be required for this.
  • Allows new row types to be defined in other modules. If some implementation comes along with some very specific requirement for a row that should appear in the cards, which is very different than any existing row, they can add that extension to a module that they own and plug it in using configuration.

It allows exactly the same amount of flexibility and configurability.

_type: Type.Array,
_element: {
_type: Type.String,
_description: 'The ID of the (bulit-in or custom) bento element',
_validators: [
validator(
(bentoElementId: string) => {
const validBentoElementIds: string[] = [...bentoElementTypes];
return validBentoElementIds.includes(bentoElementId);
},
(bentoElementId: string) => 'Invalid bento element id: ' + bentoElementId,
),
],
},
},
},
appliedTo: {
_type: Type.Array,
_elements: {
location: {
_type: Type.UUID,
_description: 'The UUID of the location. If not provided, applies to all queues.',
_default: null,
},
},
},
},
},
},
};

export interface WardConfigObject {
wardPatientCards: WardPatientCardsConfig;
}

export interface WardPatientCardsConfig {
bentoElementDefinitions: Array<BentoElementDefinition>;
cardDefinitions: Array<WardPatientCardDefinition>;
}

export interface WardPatientCardDefinition {
card: {
id: string;
header: Array<string>; // an array of (either built-in or custom) bento element ids
Copy link
Member

Choose a reason for hiding this comment

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

Convert this into a doc string (/** */)

};
appliedTo?: Array<{
location: string; // locationUuid. If given, only applies to patients at the specified ward locations. (If not provided, applies to all locations)
Copy link
Member

Choose a reason for hiding this comment

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

This would also be useful as a docstring.

}>;
}

export type BentoElementDefinition = {
id: string;
elementType: BentoElementType;
config?: BentoElementConfig;
};

export interface PatientAddressBentoElementConfig {
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 what I would expect to see, not to have this at the top level

addressFields: Array<keyof PersonAddress>;
}

export type BentoElementConfig = {} & PatientAddressBentoElementConfig;
8 changes: 6 additions & 2 deletions packages/esm-ward-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
import rootComponent from './root.component';
import {
defineConfigSchema,
getSyncLifecycle,
registerBreadcrumbs
} from '@openmrs/esm-framework';
import { configSchema } from './config-schema';
import rootComponent from './root.component';

export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');

Expand Down
78 changes: 77 additions & 1 deletion packages/esm-ward-app/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
import { type Location, type Patient } from '@openmrs/esm-framework';
import {
type OpenmrsResource,
type OpenmrsResourceStrict,
type Person,
type Visit,
type Location,
type Patient,
} from '@openmrs/esm-framework';
import type React from 'react';

export interface WardPatientCardProps {
patient: Patient;
bed: Bed;
status?: WardPatientStatus;
}

export type WardPatientCardRow = React.FC<WardPatientCardProps>;
export type WardPatientCardBentoElement = React.FC<WardPatientCardProps>;

export type WardPatientStatus = 'admitted' | 'pending';
Copy link
Member

Choose a reason for hiding this comment

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

Patients might be in a bed but not admitted.


export const bentoElementTypes = [
'bed-number',
'patient-name',
'patient-age',
'patient-address',
'admission-time',
] as const;
export type BentoElementType = (typeof bentoElementTypes)[number];

// server-side types defined in openmrs-module-bedmanagement:

Expand Down Expand Up @@ -50,3 +78,51 @@ interface BedTagMap {
}

export type BedStatus = 'AVAILABLE' | 'OCCUPIED';

// TODO: Move these types to esm-core
export interface Observation extends OpenmrsResourceStrict {
concept: OpenmrsResource;
person: Person;
obsDatetime: string;
accessionNumber: string;
obsGroup: Observation;
valueCodedName: OpenmrsResource; // ConceptName
groupMembers: Array<Observation>;
comment: string;
location: Location;
order: OpenmrsResource; // Order
encounter: Encounter;
voided: boolean;
}

export interface Encounter extends OpenmrsResourceStrict {
encounterDatetime?: string;
patient?: Patient;
location?: Location;
form?: OpenmrsResource;
encounterType?: EncounterType;
obs?: Observation;
orders?: any;
voided?: boolean;
visit?: Visit;
encounterProviders?: Array<EncounterProvider>;
diagnoses?: any;
}

export interface EncounterProvider extends OpenmrsResourceStrict {
provider?: OpenmrsResource;
encounterRole?: EncounterRole;
voided?: boolean;
}

export interface EncounterType extends OpenmrsResourceStrict {
name?: string;
description?: string;
retired?: boolean;
}

export interface EncounterRole extends OpenmrsResourceStrict {
name?: string;
description?: string;
retired?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { type WardPatientCardBentoElement } from '../../types';

const WardPatientAge: WardPatientCardBentoElement = ({ patient }) => {
const { t } = useTranslation();
return (
<div>
{t('yearsOld', '{{age}} yrs', {
age: patient?.person?.age,
})}
Comment on lines +9 to +
Copy link
Member

Choose a reason for hiding this comment

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

I'd probably suggest using esm-framework's age() function here. That handles years, months, weeks, and days (may need some extension for hours).

Copy link
Member

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The backend isn't returning the birthdate either. I'll add a TODO.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is this PR. May be we push for its review

Copy link
Member

Choose a reason for hiding this comment

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

Ah... I hadn't looked into the backend, but it looks like the fields returned from patient are hard-coded. Here's a ticket to adjust that: https://openmrs.atlassian.net/browse/BED-10.

</div>
);
};

export default WardPatientAge;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import styles from '../ward-patient-card.scss';
import { type WardPatientCardBentoElement } from '../../types';

const WardPatientBedNumber: WardPatientCardBentoElement = ({ bed }) => {
return (
<div className={styles.bedNumberBox}>
<span className={styles.wardPatientBedNumber}>{bed.bedNumber}</span>
</div>
);
};

export default WardPatientBedNumber;
Loading