-
Notifications
You must be signed in to change notification settings - Fork 349
O3-3210 ward app - configuration system for ward patient cards #1184
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
Changes from 15 commits
091498f
9260f40
8f7265e
28f952d
5a612cf
895b97c
525258a
bccda94
693f126
577059c
77d3d47
4d87d4f
3d3232b
2a316be
4d2cae4
2bd3210
1266781
a6b19f9
a00bacc
d3bcb20
a2f9640
92516fe
6bade24
9db705c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||||||
|
||||||
|
|
||||||
| 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}> | ||||||
|
||||||
| <div key={patient.uuid + ' ' + index}> | |
| <div key={`occupied-bed-pt-${patient.uuid}`}> |
Outdated
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.
An occupied bed != admitted, it just means the bed is occupied. Admission is a separate, orthongonal concern.
ibacher marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| 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'; | ||||||
|
||||||
| @import '~@openmrs/esm-styleguide/src/vars'; | |
| @use '@openmrs/esm-styleguide/src/vars'; |
Requires vars.$ui-02, etc., but slightly friendlier to the Sass compiler.
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 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?
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.
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).
| 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, | ||
| }); | ||
chibongho marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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(); | ||
| }); | ||
| }); | ||
| 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 = { | ||
|
||
| 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: { | ||
|
||
| _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: { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't like this here. A specific type of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 {
"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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
||
| _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, | ||
| ), | ||
| ], | ||
brandones marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| }, | ||
| }, | ||
| 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 | ||
|
||
| }; | ||
| appliedTo?: Array<{ | ||
| location: string; // locationUuid. If given, only applies to patients at the specified ward locations. (If not provided, applies to all locations) | ||
|
||
| }>; | ||
| } | ||
|
|
||
| export type BentoElementDefinition = { | ||
| id: string; | ||
| elementType: BentoElementType; | ||
| config?: BentoElementConfig; | ||
| }; | ||
|
|
||
| export interface PatientAddressBentoElementConfig { | ||
|
||
| addressFields: Array<keyof PersonAddress>; | ||
| } | ||
|
|
||
| export type BentoElementConfig = {} & PatientAddressBentoElementConfig; | ||
| 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'; | ||
|
||
|
|
||
| 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: | ||
|
|
||
|
|
@@ -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
+
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd probably suggest using esm-framework's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is this PR. May be we push for its review
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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 needs to be translated.