-
Notifications
You must be signed in to change notification settings - Fork 353
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 21 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,24 @@ | ||
| import React from 'react'; | ||
| import styles from './empty-bed.scss'; | ||
| import wardPatientCardStyles from '../ward-patient-card/ward-patient-card.scss'; | ||
| import { type Bed } from '../types'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| interface EmptyBedProps { | ||
| bed: Bed; | ||
| } | ||
|
|
||
| const EmptyBed: React.FC<EmptyBedProps> = ({ bed }) => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| return ( | ||
| <div className={styles.container}> | ||
| <span className={`${wardPatientCardStyles.wardPatientBedNumber} ${wardPatientCardStyles.empty}`}> | ||
| {bed.bedNumber} | ||
| </span> | ||
| <p className={styles.emptyBed}>{t('emptyBed', 'Empty bed')}</p> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default EmptyBed; |
| 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-patient-card'; | ||
|
|
||
| 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={'occupied-bed-pt-' + patient.uuid}> | ||
| <WardPatientCard patient={patient} bed={bed} /> | ||
| {!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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| @use '@carbon/styles/scss/spacing'; | ||
| @use '@carbon/styles/scss/type'; | ||
| @use '@openmrs/esm-styleguide/src/vars'; | ||
|
|
||
| .occupiedBed { | ||
| display: flex; | ||
| flex-direction: column; | ||
| background-color: vars.$ui-02; | ||
| } | ||
|
|
||
| .bedDivider { | ||
| background-color: vars.$ui-02; | ||
| color: vars.$text-02; | ||
| padding: spacing.$spacing-01; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| } | ||
|
|
||
| .bedDividerLine { | ||
| height: 1px; | ||
| background-color: vars.$ui-03; | ||
| width: 30%; | ||
| } |
| 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, defaultPatientCardElementConfig } 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 = defaultPatientCardElementConfig.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,136 @@ | ||
| import { type ConfigSchema } from '@openmrs/esm-framework'; | ||
| import { Type, validators, type ConfigSchema, type PersonAddress } from '@openmrs/esm-framework'; | ||
| import { patientCardElementTypes, type PatientCardElementType } from './types'; | ||
|
|
||
| export const configSchema: ConfigSchema = {}; | ||
| const defaultWardPatientCard: WardPatientCardDefinition = { | ||
| id: 'default-card', | ||
| rows: [ | ||
| { | ||
| rowType: 'header', | ||
| elements: ['bed-number', 'patient-name', 'patient-age', 'patient-address'], | ||
| }, | ||
| ], | ||
| appliedTo: null, | ||
| }; | ||
|
|
||
| export interface ConfigObject {} | ||
| const defaultPatientAddressFields: Array<keyof PersonAddress> = ['cityVillage', 'country']; | ||
|
|
||
| export const defaultPatientCardElementConfig: PatientCardElementConfig = { | ||
| addressFields: defaultPatientAddressFields, | ||
| }; | ||
|
|
||
| export const builtInPatientCardElements: PatientCardElementType[] = [ | ||
| 'bed-number', | ||
| 'patient-name', | ||
| 'patient-age', | ||
| 'patient-address', | ||
| 'admission-time', | ||
| ]; | ||
|
|
||
| export const configSchema: ConfigSchema = { | ||
| wardPatientCards: { | ||
| _description: 'Configure the display of ward patient cards', | ||
| patientCardElementDefinitions: { | ||
| _type: Type.Array, | ||
| _default: [], | ||
| _elements: { | ||
| id: { | ||
| _type: Type.String, | ||
| _description: 'The unique identifier for this custom patient card element', | ||
| }, | ||
| elementType: { | ||
| _type: Type.String, | ||
| _description: 'The patient card element type', | ||
| _validators: [validators.oneOf(patientCardElementTypes)], | ||
| }, | ||
| 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 patientCardElementType "patient-address", defining which address fields to show', | ||
| _default: defaultPatientAddressFields, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| 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.', | ||
| }, | ||
| rows: { | ||
| _type: Type.Array, | ||
| _elements: { | ||
| id: { | ||
| _type: Type.String, | ||
| _description: 'The unique identifier for this card row. Currently unused, but that might change.', | ||
| }, | ||
| elements: { | ||
| _type: Type.Array, | ||
| _element: { | ||
| _type: Type.String, | ||
| _description: 'The ID of the (bulit-in or custom) patient card elements to appear in this card row', | ||
| _validators: [validators.oneOf(patientCardElementTypes)], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| 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 { | ||
| patientCardElementDefinitions: Array<PatientCardElementDefinition>; | ||
| cardDefinitions: Array<WardPatientCardDefinition>; | ||
| } | ||
|
|
||
| export interface WardPatientCardDefinition { | ||
| id: string; | ||
| rows: Array<{ | ||
| /** | ||
| * The type of row. Currently, only "header" is supported | ||
| */ | ||
| rowType: 'header'; | ||
|
|
||
| /** | ||
| * an array of (either built-in or custom) patient card element ids | ||
| */ | ||
| elements: Array<string>; | ||
| }>; | ||
| appliedTo?: Array<{ | ||
| /** | ||
| * locationUuid. If given, only applies to patients at the specified ward locations. (If not provided, applies to all locations) | ||
| */ | ||
| location: string; | ||
| }>; | ||
| } | ||
|
|
||
| export type PatientCardElementDefinition = { | ||
| id: string; | ||
| elementType: PatientCardElementType; | ||
| config?: PatientCardElementConfig; | ||
| }; | ||
|
|
||
| export interface PatientAddressElementConfig { | ||
| addressFields: Array<keyof PersonAddress>; | ||
| } | ||
|
|
||
| export type PatientCardElementConfig = {} & PatientAddressElementConfig; | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.