From f0e665916cbc71e9922cf3ef0881aae3d894deff Mon Sep 17 00:00:00 2001 From: chibongho Date: Tue, 15 Oct 2024 10:44:36 -0400 Subject: [PATCH 1/3] (feat) O3-4078 - Ward App - Simplify patient card configurations (#1339) * add default ward * default ward view as an extension * stash * stash * things kind of working * reduce calls to motherAndChild endpoint * fix issue with keys in ward bed * get tests to pass * update config documentation + cleanup * fixup * fixup * fix admission request notes * add documenation of 'wards' config param * address PR comments --- packages/esm-ward-app/mock.tsx | 8 + .../src/beds/empty-bed-skeleton.tsx | 6 +- .../src/beds/empty-bed.component.tsx | 6 +- packages/esm-ward-app/src/beds/empty-bed.scss | 24 -- .../src/beds/occupied-bed.component.tsx | 35 --- .../esm-ward-app/src/beds/occupied-bed.scss | 24 -- .../src/beds/unassigned-patient.component.tsx | 20 -- .../src/beds/unassigned-patient.scss | 6 - .../src/beds/ward-bed.component.tsx | 41 +++ packages/esm-ward-app/src/beds/ward-bed.scss | 45 ++++ ...ccupied-bed.test.tsx => ward-bed.test.tsx} | 43 +-- .../config-schema-admission-request-note.ts | 9 - ...onfig-schema-extension-colored-obs-tags.ts | 91 ------- .../src/config-schema-mother-child-row.ts | 26 -- .../config-schema-pending-items-extension.ts | 29 -- packages/esm-ward-app/src/config-schema.ts | 254 +++++++++++++----- .../src/hooks/useCurrentWardCardConfig.ts | 32 --- .../src/hooks/useMotherAndChildren.ts | 4 +- packages/esm-ward-app/src/hooks/useObs.ts | 4 +- .../src/hooks/useWardPatientGrouping.ts | 2 + packages/esm-ward-app/src/index.ts | 39 +-- packages/esm-ward-app/src/root.component.tsx | 3 + packages/esm-ward-app/src/routes.json | 17 +- packages/esm-ward-app/src/types/index.ts | 23 +- .../admission-request-note-row.component.tsx | 38 +++ .../admission-request-note.extension.tsx | 32 --- .../coded-obs-tags-row.component.tsx | 108 ++++++++ .../colored-obs-tags-card-row.extension.tsx | 13 - .../card-rows/mother-child-row.component.tsx | 56 ++++ .../card-rows/mother-child-row.extension.tsx | 110 -------- ...on.tsx => pending-items-row.component.tsx} | 20 +- .../ward-patient-coded-obs-tags.tsx | 17 +- .../ward-patient-header-address.tsx | 10 +- .../row-elements/ward-patient-identifier.tsx | 13 +- .../row-elements/ward-patient-obs.resource.ts | 4 +- .../row-elements/ward-patient-obs.tsx | 23 +- ...ext.tsx => ward-patient-skeleton-text.tsx} | 0 .../ward-patient-card-element.component.tsx | 69 ----- .../ward-patient-card.component.tsx | 68 ++--- .../ward-patient-card/ward-patient-card.scss | 8 +- .../ward-patient-resource.ts | 21 +- .../admission-requests-bar.component.tsx | 16 +- .../admission-requests-bar.test.tsx | 9 +- .../ward-metrics.component.tsx | 23 +- .../ward-view-header/ward-metrics.test.tsx | 62 +---- .../ward-view-header.component.tsx | 10 +- .../default-ward-beds.component.tsx | 42 +++ ...ult-ward-patient-card-header.component.tsx | 32 +++ .../default-ward-patient-card.component.tsx | 31 +++ ...efault-ward-pending-patients.component.tsx | 52 ++++ ...ult-ward-unassigned-patients.component.tsx | 32 +++ .../default-ward-view.component.tsx | 31 +++ .../maternal-ward-beds.component.tsx | 60 +++++ ...nal-ward-patient-card-header.component.tsx | 30 +++ .../maternal-ward-patient-card.component.tsx | 44 +++ ...ternal-ward-pending-patients.component.tsx | 48 ++++ ...nal-ward-unassigned-patients.component.tsx | 32 +++ .../maternal-ward-view.component.tsx | 41 +++ .../maternal-ward-view.resource.ts | 75 ++++++ .../src/ward-view/ward-bed.component.tsx | 14 - .../src/ward-view/ward-view.component.tsx | 162 +---------- .../src/ward-view/ward-view.resource.ts | 84 +++++- .../src/ward-view/ward-view.test.tsx | 18 +- .../src/ward-view/ward.component.tsx | 106 ++++++++ ...mission-request-card-actions.component.tsx | 11 +- ...dmission-request-card-header.component.tsx | 30 +-- .../admission-request-card.component.tsx | 12 +- .../admission-request-card.scss | 7 +- .../admission-requests-workspace.test.tsx | 49 +--- .../admission-requests.workspace.tsx | 46 +--- .../admit-patient-form.test.tsx | 11 +- .../admit-patient-form.workspace.tsx | 16 +- .../patient-banner.component.tsx | 17 +- .../patient-discharge.workspace.tsx | 12 +- .../patient-bed-swap-form.component.tsx | 14 +- ...atient-transfer-request-form.component.tsx | 12 +- packages/esm-ward-app/translations/en.json | 7 +- 77 files changed, 1522 insertions(+), 1177 deletions(-) delete mode 100644 packages/esm-ward-app/src/beds/empty-bed.scss delete mode 100644 packages/esm-ward-app/src/beds/occupied-bed.component.tsx delete mode 100644 packages/esm-ward-app/src/beds/occupied-bed.scss delete mode 100644 packages/esm-ward-app/src/beds/unassigned-patient.component.tsx delete mode 100644 packages/esm-ward-app/src/beds/unassigned-patient.scss create mode 100644 packages/esm-ward-app/src/beds/ward-bed.component.tsx create mode 100644 packages/esm-ward-app/src/beds/ward-bed.scss rename packages/esm-ward-app/src/beds/{occupied-bed.test.tsx => ward-bed.test.tsx} (63%) delete mode 100644 packages/esm-ward-app/src/config-schema-admission-request-note.ts delete mode 100644 packages/esm-ward-app/src/config-schema-extension-colored-obs-tags.ts delete mode 100644 packages/esm-ward-app/src/config-schema-mother-child-row.ts delete mode 100644 packages/esm-ward-app/src/config-schema-pending-items-extension.ts delete mode 100644 packages/esm-ward-app/src/hooks/useCurrentWardCardConfig.ts create mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx delete mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note.extension.tsx create mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx delete mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx create mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.component.tsx delete mode 100644 packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.extension.tsx rename packages/esm-ward-app/src/ward-patient-card/card-rows/{pending-items-car-row.extension.tsx => pending-items-row.component.tsx} (77%) rename packages/esm-ward-app/src/ward-patient-card/row-elements/{ward-pateint-skeleton-text.tsx => ward-patient-skeleton-text.tsx} (100%) delete mode 100644 packages/esm-ward-app/src/ward-patient-card/ward-patient-card-element.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-beds.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-pending-patients.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/default-ward/default-ward-view.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-beds.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.resource.ts delete mode 100644 packages/esm-ward-app/src/ward-view/ward-bed.component.tsx create mode 100644 packages/esm-ward-app/src/ward-view/ward.component.tsx diff --git a/packages/esm-ward-app/mock.tsx b/packages/esm-ward-app/mock.tsx index d9b40cb80..ff7da8e40 100644 --- a/packages/esm-ward-app/mock.tsx +++ b/packages/esm-ward-app/mock.tsx @@ -4,6 +4,8 @@ import { useInpatientAdmission } from './src/hooks/useInpatientAdmission'; import { createAndGetWardPatientGrouping } from './src/ward-view/ward-view.resource'; import { useInpatientRequest } from './src/hooks/useInpatientRequest'; import { useWardPatientGrouping } from './src/hooks/useWardPatientGrouping'; +import { type WardViewContext } from './src/types'; +import DefaultWardPatientCardHeader from './src/ward-view/default-ward/default-ward-patient-card-header.component'; jest.mock('./src/hooks/useAdmissionLocation', () => ({ useAdmissionLocation: jest.fn(), @@ -51,4 +53,10 @@ export const mockWardPatientGroupDetails = jest.mocked(useWardPatientGrouping).m inpatientAdmissionResponse: mockInpatientAdmissionResponse(), inpatientRequestResponse: mockInpatientRequestResponse(), ...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, mockInpatientRequest), + isLoading: false, }); + +export const mockWardViewContext: WardViewContext = { + wardPatientGroupDetails: mockWardPatientGroupDetails(), + WardPatientHeader: DefaultWardPatientCardHeader, +}; diff --git a/packages/esm-ward-app/src/beds/empty-bed-skeleton.tsx b/packages/esm-ward-app/src/beds/empty-bed-skeleton.tsx index 02bf7e36c..4902e07f0 100644 --- a/packages/esm-ward-app/src/beds/empty-bed-skeleton.tsx +++ b/packages/esm-ward-app/src/beds/empty-bed-skeleton.tsx @@ -1,11 +1,11 @@ import { SkeletonIcon } from '@carbon/react'; import React from 'react'; -import styles from './empty-bed.scss'; -import WardPatientSkeletonText from '../ward-patient-card/row-elements/ward-pateint-skeleton-text'; +import styles from './ward-bed.scss'; +import WardPatientSkeletonText from '../ward-patient-card/row-elements/ward-patient-skeleton-text'; const EmptyBedSkeleton = () => { return ( -
+
diff --git a/packages/esm-ward-app/src/beds/empty-bed.component.tsx b/packages/esm-ward-app/src/beds/empty-bed.component.tsx index 1d42c1ded..54cf72d51 100644 --- a/packages/esm-ward-app/src/beds/empty-bed.component.tsx +++ b/packages/esm-ward-app/src/beds/empty-bed.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import styles from './empty-bed.scss'; +import styles from './ward-bed.scss'; import wardPatientCardStyles from '../ward-patient-card/ward-patient-card.scss'; import { type Bed } from '../types'; import { useTranslation } from 'react-i18next'; @@ -12,11 +12,11 @@ const EmptyBed: React.FC = ({ bed }) => { const { t } = useTranslation(); return ( -
+
{bed.bedNumber} -

{t('emptyBed', 'Empty bed')}

+

{t('emptyBed', 'Empty bed')}

); }; diff --git a/packages/esm-ward-app/src/beds/empty-bed.scss b/packages/esm-ward-app/src/beds/empty-bed.scss deleted file mode 100644 index 7c7dce213..000000000 --- a/packages/esm-ward-app/src/beds/empty-bed.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use '@carbon/layout'; -@use '@carbon/type'; -@use '@openmrs/esm-styleguide/src/vars' as *; - -.container { - display: flex; - gap: layout.$spacing-04; - justify-content: center; - align-items: center; - border: 1px dashed $ui-04; - padding: layout.$spacing-03 layout.$spacing-04; - height: fit-content; -} - -.container:hover:not(.skeleton) { - border: 1px solid transparent; - box-shadow: inset 0px 0px 0px 2px $color-blue-60-2; - cursor: pointer; -} - -.emptyBed { - @include type.type-style('heading-compact-01'); - color: $text-02; -} diff --git a/packages/esm-ward-app/src/beds/occupied-bed.component.tsx b/packages/esm-ward-app/src/beds/occupied-bed.component.tsx deleted file mode 100644 index 28b32971c..000000000 --- a/packages/esm-ward-app/src/beds/occupied-bed.component.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Tag } from '@carbon/react'; -import { type WardBedProps } from '../ward-view/ward-bed.component'; -import WardPatientCard from '../ward-patient-card/ward-patient-card.component'; -import styles from './occupied-bed.scss'; - -const OccupiedBed: React.FC = ({ wardPatients, bed }) => { - return ( -
- {wardPatients.map((wardPatient, index: number) => { - const last = index === wardPatients.length - 1; - return ( -
- - {!last && } -
- ); - })} -
- ); -}; - -const BedShareDivider = () => { - const { t } = useTranslation(); - return ( -
-
- {t('bedShare', 'Bed share')} -
-
- ); -}; - -export default OccupiedBed; diff --git a/packages/esm-ward-app/src/beds/occupied-bed.scss b/packages/esm-ward-app/src/beds/occupied-bed.scss deleted file mode 100644 index 5d946193d..000000000 --- a/packages/esm-ward-app/src/beds/occupied-bed.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use '@carbon/layout'; -@use '@openmrs/esm-styleguide/src/vars'; - -.occupiedBed { - display: flex; - flex-direction: column; - background-color: vars.$ui-02; - height: fit-content; -} - -.bedDivider { - background-color: vars.$ui-02; - color: vars.$text-02; - padding: layout.$spacing-01; - display: flex; - align-items: center; - justify-content: space-between; -} - -.bedDividerLine { - height: 1px; - background-color: vars.$ui-05; - width: 30%; -} diff --git a/packages/esm-ward-app/src/beds/unassigned-patient.component.tsx b/packages/esm-ward-app/src/beds/unassigned-patient.component.tsx deleted file mode 100644 index ea3b4e55a..000000000 --- a/packages/esm-ward-app/src/beds/unassigned-patient.component.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import WardPatientCard from '../ward-patient-card/ward-patient-card.component'; -import styles from './unassigned-patient.scss'; -import { type WardPatient } from '../types'; - -export interface UnassignedPatientProps { - wardPatient: WardPatient; -} - -const UnassignedPatient: React.FC = ({ wardPatient }) => { - return ( -
-
- -
-
- ); -}; - -export default UnassignedPatient; diff --git a/packages/esm-ward-app/src/beds/unassigned-patient.scss b/packages/esm-ward-app/src/beds/unassigned-patient.scss deleted file mode 100644 index 539370020..000000000 --- a/packages/esm-ward-app/src/beds/unassigned-patient.scss +++ /dev/null @@ -1,6 +0,0 @@ -@use '@openmrs/esm-styleguide/src/vars'; - -.unassignedPatient { - display: flex; - flex-direction: column; -} diff --git a/packages/esm-ward-app/src/beds/ward-bed.component.tsx b/packages/esm-ward-app/src/beds/ward-bed.component.tsx new file mode 100644 index 000000000..d19c428d9 --- /dev/null +++ b/packages/esm-ward-app/src/beds/ward-bed.component.tsx @@ -0,0 +1,41 @@ +import React, { type ReactNode } from 'react'; +import { type Bed } from '../types'; +import EmptyBed from './empty-bed.component'; +import styles from './ward-bed.scss'; +import { useTranslation } from 'react-i18next'; +import { Tag } from '@carbon/react'; + +export interface WardBedProps { + patientCards: Array; + bed: Bed; +} + +const WardBed: React.FC = ({ bed, patientCards }) => { + return patientCards?.length > 0 ? : ; +}; + +const OccupiedBed: React.FC = ({ patientCards }) => { + // interlace patient card with bed dividers between each of them + const patientCardsWithDividers = patientCards.flatMap((patientCard, index) => { + if (index == 0) { + return [patientCard]; + } else { + return [, patientCard]; + } + }); + + return
{patientCardsWithDividers}
; +}; + +const BedShareDivider = () => { + const { t } = useTranslation(); + return ( +
+
+ {t('bedShare', 'Bed share')} +
+
+ ); +}; + +export default WardBed; diff --git a/packages/esm-ward-app/src/beds/ward-bed.scss b/packages/esm-ward-app/src/beds/ward-bed.scss new file mode 100644 index 000000000..472a3d126 --- /dev/null +++ b/packages/esm-ward-app/src/beds/ward-bed.scss @@ -0,0 +1,45 @@ +@use '@carbon/layout'; +@use '@openmrs/esm-styleguide/src/vars'; +@use '@carbon/type'; + +.occupiedBed { + display: flex; + flex-direction: column; + background-color: vars.$ui-02; + height: fit-content; +} + +.bedDivider { + background-color: vars.$ui-02; + color: vars.$text-02; + padding: layout.$spacing-01; + display: flex; + align-items: center; + justify-content: space-between; +} + +.bedDividerLine { + height: 1px; + background-color: vars.$ui-05; + width: 30%; +} +.emptyBed { + display: flex; + gap: layout.$spacing-04; + justify-content: center; + align-items: center; + border: 1px dashed vars.$ui-04; + padding: layout.$spacing-03 layout.$spacing-04; + height: fit-content; +} + +.emptyBed:hover:not(.skeleton) { + border: 1px solid transparent; + box-shadow: inset 0px 0px 0px 2px vars.$color-blue-60-2; + cursor: pointer; +} + +.emptyBedText { + @include type.type-style('heading-compact-01'); + color: vars.$text-02; +} \ No newline at end of file diff --git a/packages/esm-ward-app/src/beds/occupied-bed.test.tsx b/packages/esm-ward-app/src/beds/ward-bed.test.tsx similarity index 63% rename from packages/esm-ward-app/src/beds/occupied-bed.test.tsx rename to packages/esm-ward-app/src/beds/ward-bed.test.tsx index 65057ad96..5679bb119 100644 --- a/packages/esm-ward-app/src/beds/occupied-bed.test.tsx +++ b/packages/esm-ward-app/src/beds/ward-bed.test.tsx @@ -10,7 +10,9 @@ import { } from '../../../../__mocks__'; import { bedLayoutToBed, filterBeds } from '../ward-view/ward-view.resource'; import useWardLocation from '../hooks/useWardLocation'; -import OccupiedBed from './occupied-bed.component'; +import WardBed from './ward-bed.component'; +import { type WardPatient } from '../types'; +import DefaultWardPatientCard from '../ward-view/default-ward/default-ward-patient-card.component'; const defaultConfig: WardConfigObject = getDefaultsFromConfigSchema(configSchema); @@ -31,34 +33,43 @@ mockedUseWardLocation.mockReturnValue({ const mockBedToUse = mockBedLayouts[0]; const mockBed = bedLayoutToBed(mockBedToUse); -const mockWardPatientProps = { - admitted: true, +const mockWardPatientAliceProps: WardPatient = { visit: null, - encounterAssigningToCurrentInpatientLocation: null, - firstAdmissionOrTransferEncounter: null, + patient: mockPatientAlice, + bed: mockBed, + inpatientAdmission: null, + inpatientRequest: null, }; -describe('Occupied bed', () => { +const mockWardPatientBrianProps: WardPatient = { + visit: null, + patient: mockPatientBrian, + bed: mockBed, + inpatientAdmission: null, + inpatientRequest: null, +}; + +describe('Ward bed', () => { it('renders a single bed with patient details', () => { - render(); + render( + ]} + bed={mockBed} + />, + ); const patientName = screen.getByText('Alice Johnson'); expect(patientName).toBeInTheDocument(); const patientAge = `${mockPatientAlice.person.age} yrs`; expect(screen.getByText(patientAge)).toBeInTheDocument(); - const defaultAddressFields = ['cityVillage', 'country']; - defaultAddressFields.forEach((addressField) => { - const addressFieldValue = mockPatientAlice.person.preferredAddress[addressField] as string; - expect(screen.getByText(addressFieldValue)).toBeInTheDocument(); - }); }); it('renders a divider for shared patients', () => { render( - , + , ]} />, ); diff --git a/packages/esm-ward-app/src/config-schema-admission-request-note.ts b/packages/esm-ward-app/src/config-schema-admission-request-note.ts deleted file mode 100644 index 9590b657d..000000000 --- a/packages/esm-ward-app/src/config-schema-admission-request-note.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Type } from '@openmrs/esm-framework'; - -export const admissionRequestNoteRowConfigSchema = { - conceptUuid: { - _type: Type.UUID, - _description: 'Required. Identifies the concept for the admission request note.', - _default: '161011AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - }, -}; diff --git a/packages/esm-ward-app/src/config-schema-extension-colored-obs-tags.ts b/packages/esm-ward-app/src/config-schema-extension-colored-obs-tags.ts deleted file mode 100644 index 5deea89a9..000000000 --- a/packages/esm-ward-app/src/config-schema-extension-colored-obs-tags.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Type } from '@openmrs/esm-framework'; - -export const coloredObsTagsCardRowConfigSchema = { - conceptUuid: { - _type: Type.UUID, - _description: 'Required. Identifies the concept to use to identify the desired observations.', - // Problem list - _default: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - }, - 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: { - _type: Type.Array, - _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.`, - _default: [], - _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, - }, - }, - }, - }, -}; - -export interface ColoredObsTagsCardRowConfigObject { - /** - * 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; -} - -export interface TagConfigObject { - /** - * 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; -} diff --git a/packages/esm-ward-app/src/config-schema-mother-child-row.ts b/packages/esm-ward-app/src/config-schema-mother-child-row.ts deleted file mode 100644 index 0e83e4dab..000000000 --- a/packages/esm-ward-app/src/config-schema-mother-child-row.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Type } from '@openmrs/esm-framework'; - -export const motherChildRowConfigSchema = { - maternalLocations: { - _type: Type.Array, - _description: 'Defines obs display elements that can be included in the card header or footer.', - _default: [], - _elements: { - id: { - _type: Type.UUID, - _description: 'The unique identifier for this ward location', - }, - } - }, - childLocations: { - _type: Type.Array, - _description: 'Defines obs display elements that can be included in the card header or footer.', - _default: [], - _elements: { - id: { - _type: Type.UUID, - _description: 'The unique identifier for this ward location', - }, - } - } -}; diff --git a/packages/esm-ward-app/src/config-schema-pending-items-extension.ts b/packages/esm-ward-app/src/config-schema-pending-items-extension.ts deleted file mode 100644 index 985b5a1b2..000000000 --- a/packages/esm-ward-app/src/config-schema-pending-items-extension.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Type } from '@openmrs/esm-framework'; - -export const pendingItemsExtensionConfigSchema = { - orders: { - orderTypes: { - _type: Type.Array, - _description: 'Defines order types displayed on the ward patient card pending items section.', - _default: [{ label: 'Labs', uuid: '52a447d3-a64a-11e3-9aeb-50e549534c5e' }], - _elements: { - uuid: { - _type: Type.UUID, - _description: 'Identifies the order type.', - }, - label: { - _type: Type.String, - _description: - "The label or i18n key to the translated label to display. If not provided, defaults to 'Orders'", - _default: null, - }, - }, - }, - }, - showPendingItems: { - _type: Type.Boolean, - _description: - 'Optional. If true, pending items (e.g., number of pending orders) will be displayed on the patient card.', - _default: true, - }, -}; diff --git a/packages/esm-ward-app/src/config-schema.ts b/packages/esm-ward-app/src/config-schema.ts index a4dfb5945..dc79cd2a9 100644 --- a/packages/esm-ward-app/src/config-schema.ts +++ b/packages/esm-ward-app/src/config-schema.ts @@ -1,12 +1,5 @@ import { type ConfigSchema, Type, validators } from '@openmrs/esm-framework'; -export const defaultWardPatientCard: WardPatientCardDefinition = { - id: 'default', - headerRowElements: ['patient-age', 'patient-address', 'patient-identifier'], - footerRowElements: [], - appliedTo: null, -}; - export const builtInPatientCardElements = ['patient-age', 'time-on-ward', 'time-since-admission', 'patient-location']; export const addressFields = [ @@ -37,11 +30,12 @@ export const addressFields = [ type AddressField = keyof typeof addressFields; export const configSchema: ConfigSchema = { - wardPatientCards: { - _description: 'Configure the display of ward patient cards', - obsElementDefinitions: { + patientCardElements: { + _description: + 'Configuration of various patient card elements. Each configured element must have a unique id, defined in the ward React component being used.', + obs: { _type: Type.Array, - _description: 'Defines obs display elements that can be included in the card header or footer.', + _description: 'Configures obs values to display.', _default: [], _elements: { id: { @@ -79,14 +73,52 @@ export const configSchema: ConfigSchema = { }, }, }, - identifierElementDefinitions: { + pendingItems: { _type: Type.Array, - _description: `Defines patient identifier elements that can be included in the card header or footer. The default element 'patient-identifier' displays the preferred identifier.`, + _description: 'Configures pending orders and transfers to display.', _default: [ { - id: 'patient-identifier', + id: 'pending-items', + orders: { + orderTypes: [{ label: 'Labs', uuid: '52a447d3-a64a-11e3-9aeb-50e549534c5e' }], + }, + showPendingItems: true, }, ], + _elements: { + id: { + _type: Type.String, + _description: 'The unique identifier for this patient card element', + }, + orders: { + orderTypes: { + _type: Type.Array, + _description: 'Configures pending orders and transfers to display.', + _elements: { + uuid: { + _type: Type.UUID, + _description: 'Identifies the order type.', + }, + label: { + _type: Type.String, + _description: + "The label or i18n key to the translated label to display. If not provided, defaults to 'Orders'", + _default: null, + }, + }, + }, + }, + showPendingItems: { + _type: Type.Boolean, + _description: + 'Optional. If true, pending items (e.g., number of pending orders) will be displayed on the patient card.', + }, + }, + }, + patientIdentifier: { + _type: Type.Array, + _description: `Configures patient identifier to display. An unconfigured element displays the preferred identifier.`, + _default: [], _elements: { id: { _type: Type.String, @@ -106,9 +138,9 @@ export const configSchema: ConfigSchema = { }, }, }, - addressElementDefinitions: { + patientAddress: { _type: Type.Array, - _description: 'Defines patient address elements that can be included in the card header or footer.', + _description: 'Configures patient address elements.', _default: [ { id: 'patient-address', @@ -132,66 +164,116 @@ export const configSchema: ConfigSchema = { }, }, }, - cardDefinitions: { + admissionRequestNote: { _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.`, + _description: 'Configures admission request notes to display.', + _default: [ + { + id: 'admission-request-note', + conceptUuid: '161011AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + ], _elements: { - id: { - _type: Type.String, - _description: - 'The unique identifier for this card definition. This is used to set the name of the extension slot the card has, where the rows go. The slot name is "ward-patient-card-", unless the id is "default", in which case the slot name is "ward-patient-card".', - _default: 'default', - }, - headerRowElements: { - _type: Type.Array, - _description: `IDs of patient card elements to appear in the header row. These can be built-in, or custom ones can be defined in patientCardElementDefinitions. Built-in elements are: '${builtInPatientCardElements.join( - "', '", - )}'.`, - _elements: { + fields: { + id: { _type: Type.String, + _description: 'The unique identifier for this patient card element', + }, + conceptUuid: { + _type: Type.UUID, + _description: 'Required. Identifies the concept for the admission request note.', }, }, - footerRowElements: { - _type: Type.Array, - _description: `IDs of patient card elements to appear in the footer row. These can be built-in, or custom ones can be defined in patientCardElementDefinitions. Built-in elements are: '${builtInPatientCardElements.join( - "', '", - )}'.`, - _elements: { + }, + coloredObsTags: { + _type: Type.Array, + _description: 'Configures observation values to display as Carbon tags.', + _elements: { + conceptUuid: { + _type: Type.UUID, + _description: 'Required. Identifies the concept to use to identify the desired observations.', + // Problem list + _default: '1284AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + 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, }, - }, - appliedTo: { - _type: Type.Array, - _description: - 'Conditions under which this card definition should be used. If not provided, the configuration is applied to all wards.', - _elements: { - location: { - _type: Type.UUID, - _description: 'The UUID of the location. If not provided, applies to all wards.', - _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: { + _type: Type.Array, + _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.`, + _default: [], + _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, + }, + }, }, }, }, }, }, }, + wards: { + _description: 'Configuration of what type of ward to use at different ward locations.', + _type: Type.Array, + _default: [{ id: 'default-ward' }], + _elements: { + id: { + _type: Type.String, + _description: + 'The ward type to use. Currently, "default-ward" and "maternal-ward" are supported. This string also serves as the extension slot name for the ward view.', + }, + appliedTo: { + _type: Type.Array, + _description: + 'Optional. Conditions under which this card definition should be used. If not provided, the configuration is applied to all wards.', + _elements: { + location: { + _type: Type.UUID, + _description: 'The UUID of the location. If not provided, applies to all wards.', + _default: null, + }, + }, + }, + }, + }, }; export interface WardConfigObject { - wardPatientCards: WardPatientCardsConfig; -} - -export interface WardPatientCardsConfig { - obsElementDefinitions: Array; - pendingItemsDefinitions: Array; - identifierElementDefinitions: Array; - addressElementDefinitions: Array; - cardDefinitions: Array; + patientCardElements: { + obs: Array; + pendingItems: Array; + patientIdentifier: Array; + patientAddress: Array; + coloredObsTags: Array; + admissionRequestNote: Array; + }; + wards: Array; } -export interface PendingItemsDefinition { +export interface PendingItemsElementConfig { + id: string; showPendingItems: boolean; orders: { orderTypes: Array<{ @@ -201,7 +283,7 @@ export interface PendingItemsDefinition { }; } -export interface ObsElementDefinition { +export interface ObsElementConfig { id: string; conceptUuid: string; onlyWithinCurrentVisit: boolean; @@ -210,21 +292,24 @@ export interface ObsElementDefinition { label?: string; } -export interface IdentifierElementDefinition { +export interface IdentifierElementConfig { id: string; identifierTypeUuid: string; label?: string; } -export interface AddressElementDefinition { +export interface PatientAddressElementConfig { id: string; fields: Array; } -export interface WardPatientCardDefinition { +export interface AdmissionRequestNoteElementConfig { + id: string; + conceptUuid: string; +} + +export interface WardDefinition { id: string; - headerRowElements: Array; - footerRowElements: Array; appliedTo?: Array<{ /** * locationUuid. If given, only applies to patients at the specified ward locations. (If not provided, applies to all locations) @@ -232,3 +317,44 @@ export interface WardPatientCardDefinition { location: string; }>; } +export interface ColoredObsTagsElementConfig { + /** + * 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; + + /** + * 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; +} + +export interface ColoredObsTagConfig { + /** + * 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; +} diff --git a/packages/esm-ward-app/src/hooks/useCurrentWardCardConfig.ts b/packages/esm-ward-app/src/hooks/useCurrentWardCardConfig.ts deleted file mode 100644 index 9df86121e..000000000 --- a/packages/esm-ward-app/src/hooks/useCurrentWardCardConfig.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useConfig } from '@openmrs/esm-framework'; -import { useMemo } from 'react'; -import { type WardConfigObject, defaultWardPatientCard } from '../config-schema'; -import useWardLocation from './useWardLocation'; - -export function useCurrentWardCardConfig() { - const { wardPatientCards } = useConfig(); - const { - location: { uuid: locationUuid }, - } = useWardLocation(); - - const currentWardCardConfig = useMemo(() => { - const cardDefinition = wardPatientCards.cardDefinitions.find((cardDef) => { - return ( - cardDef.appliedTo == null || - cardDef.appliedTo?.length == 0 || - cardDef.appliedTo.some((criteria) => criteria.location == locationUuid) - ); - }); - - return cardDefinition; - }, [wardPatientCards, locationUuid]); - - if (!currentWardCardConfig) { - console.warn( - 'No ward card configuration has `appliedTo` criteria that matches the current location. Using the default configuration.', - ); - return defaultWardPatientCard; - } - - return currentWardCardConfig; -} diff --git a/packages/esm-ward-app/src/hooks/useMotherAndChildren.ts b/packages/esm-ward-app/src/hooks/useMotherAndChildren.ts index 8ecbce4b2..03112faf5 100644 --- a/packages/esm-ward-app/src/hooks/useMotherAndChildren.ts +++ b/packages/esm-ward-app/src/hooks/useMotherAndChildren.ts @@ -1,5 +1,5 @@ import { makeUrl, restBaseUrl, useOpenmrsFetchAll } from '@openmrs/esm-framework'; -import { type MotherAndChildren } from '../types'; +import { type MotherAndChild } from '../types'; export interface MothersAndChildrenSearchCriteria { mothers?: Array; @@ -38,7 +38,7 @@ export function useMotherAndChildren( requireChildBornDuringMothersActiveVisit?.toString() ?? 'false', ); rep && url.searchParams.append('v', rep); - return useOpenmrsFetchAll(fetch ? url : null); + return useOpenmrsFetchAll(fetch ? url : null); } function makeUrlUrl(path: string) { diff --git a/packages/esm-ward-app/src/hooks/useObs.ts b/packages/esm-ward-app/src/hooks/useObs.ts index 620fbe332..20d085999 100644 --- a/packages/esm-ward-app/src/hooks/useObs.ts +++ b/packages/esm-ward-app/src/hooks/useObs.ts @@ -6,12 +6,12 @@ interface ObsSearchCriteria { concept: string; } -export function useObs(criteria?: ObsSearchCriteria, representation = 'default') { +export function useObs(criteria?: ObsSearchCriteria, fetch: boolean = true, representation = 'default') { const params = new URLSearchParams({ ...criteria, v: representation, }); const apiUrl = `${restBaseUrl}/obs?${params}`; - return useOpenmrsFetchAll(apiUrl); + return useOpenmrsFetchAll(fetch ? apiUrl : null); } diff --git a/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts index e3284385b..f6c43b43f 100644 --- a/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts +++ b/packages/esm-ward-app/src/hooks/useWardPatientGrouping.ts @@ -21,5 +21,7 @@ export function useWardPatientGrouping() { admissionLocationResponse, inpatientAdmissionResponse, inpatientRequestResponse, + isLoading: + admissionLocationResponse.isLoading || inpatientAdmissionResponse.isLoading || inpatientRequestResponse.isLoading, }; } diff --git a/packages/esm-ward-app/src/index.ts b/packages/esm-ward-app/src/index.ts index 2d56c54f8..d7a3da389 100644 --- a/packages/esm-ward-app/src/index.ts +++ b/packages/esm-ward-app/src/index.ts @@ -1,19 +1,14 @@ import { defineConfigSchema, - defineExtensionConfigSchema, getAsyncLifecycle, getSyncLifecycle, registerBreadcrumbs, registerFeatureFlag, } from '@openmrs/esm-framework'; import { configSchema } from './config-schema'; -import { admissionRequestNoteRowConfigSchema } from './config-schema-admission-request-note'; -import { coloredObsTagsCardRowConfigSchema } from './config-schema-extension-colored-obs-tags'; import { moduleName } from './constant'; import { createDashboardLink } from './createDashboardLink.component'; import rootComponent from './root.component'; -import { motherChildRowConfigSchema } from './config-schema-mother-child-row'; -import { pendingItemsExtensionConfigSchema } from './config-schema-pending-items-extension'; export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); @@ -59,26 +54,6 @@ export const wardPatientNotesActionButtonExtension = getAsyncLifecycle( options, ); -export const coloredObsTagCardRowExtension = getAsyncLifecycle( - () => import('./ward-patient-card/card-rows/colored-obs-tags-card-row.extension'), - options, -); - -export const admissionRequestNoteRowExtension = getAsyncLifecycle( - () => import('./ward-patient-card/card-rows/admission-request-note.extension'), - options, -); - -export const motherChildRowExtension = getAsyncLifecycle( - () => import('./ward-patient-card/card-rows/mother-child-row.extension'), - options, -); - -export const pendingItemsCardRowExtension = getAsyncLifecycle( - () => import('./ward-patient-card/card-rows/pending-items-car-row.extension'), - options, -); - // t('transfers', 'Transfers') export const patientTransferAndSwapWorkspace = getAsyncLifecycle( () => import('./ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace'), @@ -117,13 +92,19 @@ export const clinicalFormWorkspaceSideRailIcon = getAsyncLifecycle( options, ); +export const defaultWardView = getAsyncLifecycle( + () => import('./ward-view/default-ward/default-ward-view.component'), + options, +); + +export const maternalWardView = getAsyncLifecycle( + () => import('./ward-view/materal-ward/maternal-ward-view.component'), + options, +); + export function startupApp() { registerBreadcrumbs([]); defineConfigSchema(moduleName, configSchema); - defineExtensionConfigSchema('colored-obs-tags-card-row', coloredObsTagsCardRowConfigSchema); - defineExtensionConfigSchema('admission-request-note-card-row', admissionRequestNoteRowConfigSchema); - defineExtensionConfigSchema('mother-child-card-row', motherChildRowConfigSchema); - defineExtensionConfigSchema('ward-patient-pending-items-card-row', pendingItemsExtensionConfigSchema); registerFeatureFlag( 'bedmanagement-module', diff --git a/packages/esm-ward-app/src/root.component.tsx b/packages/esm-ward-app/src/root.component.tsx index 18560fc4a..972cb69ad 100644 --- a/packages/esm-ward-app/src/root.component.tsx +++ b/packages/esm-ward-app/src/root.component.tsx @@ -1,3 +1,4 @@ +import { WorkspaceContainer } from '@openmrs/esm-framework'; import React from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import WardView from './ward-view/ward-view.component'; @@ -14,6 +15,8 @@ const Root: React.FC = () => { } /> + + ); }; diff --git a/packages/esm-ward-app/src/routes.json b/packages/esm-ward-app/src/routes.json index 7323ee840..2ec17081e 100644 --- a/packages/esm-ward-app/src/routes.json +++ b/packages/esm-ward-app/src/routes.json @@ -61,19 +61,14 @@ "slot": "action-menu-ward-patient-items-slot" }, { - "component": "admissionRequestNoteRowExtension", - "name": "admission-request-note-card-row", - "slot": "ward-patient-card-slot" - }, - { - "component": "motherChildRowExtension", - "name": "mother-child-card-row", - "slot": "ward-patient-card-slot" + "component": "defaultWardView", + "name": "default-ward", + "slot": "default-ward" }, { - "component": "pendingItemsCardRowExtension", - "name": "ward-patient-pending-items-card-row", - "slot": "ward-patient-card-pending-items-slot" + "component": "maternalWardView", + "name": "maternal-ward", + "slot": "maternal-ward" } ], "workspaces": [ diff --git a/packages/esm-ward-app/src/types/index.ts b/packages/esm-ward-app/src/types/index.ts index 37babdac2..92fc41166 100644 --- a/packages/esm-ward-app/src/types/index.ts +++ b/packages/esm-ward-app/src/types/index.ts @@ -11,7 +11,7 @@ import type { import type React from 'react'; import type { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; -export type WardPatientCard = React.FC; +export type WardPatientCardType = React.FC; // WardPatient is a patient admitted to a ward, and/or in a bed on a ward export type WardPatient = { @@ -44,10 +44,7 @@ export type WardPatient = { export interface WardPatientWorkspaceProps extends DefaultWorkspaceProps { wardPatient: WardPatient; -} -export interface MotherAndChildrenRelationships { - motherByChildUuid: Map; - childrenByMotherUuid: Map>; + WardPatientHeader: React.FC; } // server-side types defined in openmrs-module-bedmanagement: @@ -144,9 +141,6 @@ export interface InpatientAdmission { // the current in patient request currentInpatientRequest: InpatientRequest; } -export interface WardAppContext { - allPatientsByPatientUuid: Map; -} export interface MotherAndChild { mother: Patient; @@ -228,11 +222,12 @@ export interface ObsPayload { groupMembers?: Array; } -export interface MotherAndChildren { - childAdmission: InpatientAdmission; - child: Patient; - motherAdmission: InpatientAdmission; - mother: Patient; +export type WardPatientGroupDetails = ReturnType; +export interface WardViewContext { + wardPatientGroupDetails: WardPatientGroupDetails; + WardPatientHeader: React.FC; } -export type WardPatientGroupDetails = ReturnType; +export interface MaternalWardViewContext { + motherChildrenRelationshipsByPatient: Map; +} diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx new file mode 100644 index 000000000..c44918280 --- /dev/null +++ b/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note-row.component.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { type ObsElementConfig } from '../../config-schema'; +import { type WardPatient } from '../../types'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; +import WardPatientObs from '../row-elements/ward-patient-obs'; +import styles from '../ward-patient-card.scss'; + +interface AdmissionRequestNoteRowProps { + wardPatient: WardPatient; + id: string; +} + +const AdmissionRequestNoteRow: React.FC = ({ id, wardPatient }) => { + const { patient, visit, inpatientAdmission } = wardPatient; + const { conceptUuid } = useElementConfig('admissionRequestNote', id) ?? {}; + const config: ObsElementConfig = { + conceptUuid, + limit: 0, + id: 'admission-note', + onlyWithinCurrentVisit: true, + orderBy: 'ascending', + label: 'Admission Note', + }; + + // only show if the patient has not been admitted yet + const admitted = inpatientAdmission != null; + if (admitted) { + return null; + } else { + return ( +
+ +
+ ); + } +}; + +export default AdmissionRequestNoteRow; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note.extension.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note.extension.tsx deleted file mode 100644 index 9d7f32046..000000000 --- a/packages/esm-ward-app/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { type ObsElementDefinition } from '../../config-schema'; -import { type WardPatientCard } from '../../types'; -import WardPatientObs from '../row-elements/ward-patient-obs'; -import { useConfig } from '@openmrs/esm-framework'; -import styles from '../ward-patient-card.scss'; - -const AdmissionRequestNoteRowExtension: WardPatientCard = ({ patient, visit, inpatientAdmission }) => { - const { conceptUuid } = useConfig(); - const config: ObsElementDefinition = { - conceptUuid, - limit: 0, - id: 'admission-note', - onlyWithinCurrentVisit: true, - orderBy: 'ascending', - label: 'Admission Note', - }; - - // only show if the patient has not been admitted yet - const admitted = inpatientAdmission != null; - if (admitted) { - return null; - } else { - return ( -
- -
- ); - } -}; - -export default AdmissionRequestNoteRowExtension; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx new file mode 100644 index 000000000..009cc2797 --- /dev/null +++ b/packages/esm-ward-app/src/ward-patient-card/card-rows/coded-obs-tags-row.component.tsx @@ -0,0 +1,108 @@ +import { Tag } from '@carbon/react'; +import { type OpenmrsResource, type Patient, type Visit } from '@openmrs/esm-framework'; +import React, { type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useObs } from '../../hooks/useObs'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; +import styles from '../ward-patient-card.scss'; +import WardPatientSkeletonText from '../row-elements/ward-patient-skeleton-text'; +import { + getObsEncounterString, + obsCustomRepresentation, + useConceptToTagColorMap, +} from '../row-elements/ward-patient-obs.resource'; +import WardPatientResponsiveTooltip from '../row-elements/ward-patient-responsive-tooltip'; + +interface WardPatientCodedObsTagsRowProps { + id: string; + patient: Patient; + visit: Visit; +} + +/** + * 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 CodedObsTagsRow: React.FC = ({ id, patient, visit }) => { + const config = useElementConfig('coloredObsTags', id); + const { conceptUuid, summaryLabel, summaryLabelColor } = config ?? {}; + const { data, isLoading } = useObs( + { patient: patient.uuid, concept: conceptUuid }, + conceptUuid != null, + obsCustomRepresentation, + ); + const { t } = useTranslation(); + const conceptToTagColorMap = useConceptToTagColorMap(config.tags); + + if (isLoading) { + return ( +
+ +
+ ); + } else { + const obsToDisplay = data?.filter((o) => { + const matchVisit = o.encounter.visit?.uuid == visit?.uuid; + return matchVisit; + }); + + const summaryLabelToDisplay = summaryLabel != null ? t(summaryLabel) : obsToDisplay?.[0]?.concept?.display; + + // for each obs configured to be displayed with a color, we create a tag for it + // for other obs not configured, we create a single summary tag for all of them. + const summaryTagTooltipText: ReactNode[] = []; + const coloredOpsTags = obsToDisplay + ?.map((o) => { + const { display, uuid } = o.value as OpenmrsResource; + + const color = conceptToTagColorMap?.get(uuid); + if (color) { + return ( + + + {display} + + + ); + } else { + summaryTagTooltipText.push( +
+ {display} ({getObsEncounterString(o, t)}) +
, + ); + return null; + } + }) + .filter((o) => o != null); + + if (coloredOpsTags?.length > 0 || summaryTagTooltipText.length > 0) { + return ( +
+ + {coloredOpsTags} + {summaryTagTooltipText.length > 0 ? ( + + + {t('countItems', '{{count}} {{item}}', { + count: summaryTagTooltipText.length, + item: summaryLabelToDisplay, + })} + + + ) : null} + +
+ ); + } else { + return null; + } + } +}; + +export default CodedObsTagsRow; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx deleted file mode 100644 index 5a6178925..000000000 --- a/packages/esm-ward-app/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useConfig } from '@openmrs/esm-framework'; -import React from 'react'; -import { type ColoredObsTagsCardRowConfigObject } from '../../config-schema-extension-colored-obs-tags'; -import { type WardPatientCard } from '../../types'; -import WardPatientCodedObsTags from '../row-elements/ward-patient-coded-obs-tags'; - -const ColoredObsTagsCardRowExtension: WardPatientCard = ({ patient, visit }) => { - const config = useConfig(); - - return ; -}; - -export default ColoredObsTagsCardRowExtension; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.component.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.component.tsx new file mode 100644 index 000000000..d1a75f80f --- /dev/null +++ b/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.component.tsx @@ -0,0 +1,56 @@ +import { BabyIcon, MotherIcon, useAppContext } from '@openmrs/esm-framework'; +import classNames from 'classnames'; +import React from 'react'; +import { type MaternalWardViewContext, type WardPatientCardType } from '../../types'; +import WardPatientAge from '../row-elements/ward-patient-age'; +import WardPatientIdentifier from '../row-elements/ward-patient-identifier'; +import WardPatientLocation from '../row-elements/ward-patient-location'; +import WardPatientName from '../row-elements/ward-patient-name'; +import wardPatientCardStyles from '../ward-patient-card.scss'; +import styles from './mother-child-row.scss'; + +/** + * This extension displays the mother or children of the patient in the patient card. + * + * @param param0 + * @returns + */ +const MotherChildRowExtension: WardPatientCardType = ({ patient }) => { + const { motherChildrenRelationshipsByPatient } = + useAppContext('maternal-ward-view-context') ?? {}; + + const motherChildRelationships = motherChildrenRelationshipsByPatient?.get(patient.uuid) ?? []; + return ( + <> + {motherChildRelationships.map(({ mother, motherAdmission, child, childAdmission }) => { + // patient A is the patient card's patient + const patientA = patient; + // patient B is either the mother or the child of patient A + const isPatientBTheMother = mother.uuid != patientA.uuid; + const patientB = isPatientBTheMother ? mother : child; + + // we display patient B here + const Icon = isPatientBTheMother ? MotherIcon : BabyIcon; + const patientBAdmission = isPatientBTheMother ? motherAdmission : childAdmission; + + return ( +
+
+ +
+
+ + + + +
+
+ ); + })} + + ); +}; + +export default MotherChildRowExtension; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.extension.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.extension.tsx deleted file mode 100644 index ec40f6103..000000000 --- a/packages/esm-ward-app/src/ward-patient-card/card-rows/mother-child-row.extension.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { InlineNotification } from '@carbon/react'; -import { BabyIcon, MotherIcon } from '@openmrs/esm-framework'; -import classNames from 'classnames'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { type MothersAndChildrenSearchCriteria, useMotherAndChildren } from '../../hooks/useMotherAndChildren'; -import { type WardPatientCard } from '../../types'; -import WardPatientSkeletonText from '../row-elements/ward-pateint-skeleton-text'; -import WardPatientAge from '../row-elements/ward-patient-age'; -import WardPatientIdentifier from '../row-elements/ward-patient-identifier'; -import WardPatientLocation from '../row-elements/ward-patient-location'; -import WardPatientName from '../row-elements/ward-patient-name'; -import wardPatientCardStyles from '../ward-patient-card.scss'; -import styles from './mother-child-row.scss'; - -const motherAndChildrenRep = - 'custom:(childAdmission,mother:(person,identifiers:full,uuid),child:(person,identifiers:full,uuid),motherAdmission)'; - -/** - * This extension displays the mother or children of the patient in the patient card. - * - * @param param0 - * @returns - */ -const MotherChildRowExtension: WardPatientCard = ({ patient }) => { - const { t } = useTranslation(); - - const getChildrenRequestParams: MothersAndChildrenSearchCriteria = { - mothers: [patient.uuid], - requireMotherHasActiveVisit: true, - requireChildHasActiveVisit: true, - requireChildBornDuringMothersActiveVisit: true, - }; - - const getMotherRequestParams: MothersAndChildrenSearchCriteria = { - children: [patient.uuid], - requireMotherHasActiveVisit: true, - requireChildHasActiveVisit: true, - requireChildBornDuringMothersActiveVisit: true, - }; - - const { - data: childrenData, - isLoading: isLoadingChildrenData, - error: childrenDataError, - } = useMotherAndChildren(getChildrenRequestParams, true, motherAndChildrenRep); - const { - data: motherData, - isLoading: isLoadingMotherData, - error: motherDataError, - } = useMotherAndChildren(getMotherRequestParams, true, motherAndChildrenRep); - - if (isLoadingChildrenData || isLoadingMotherData) { - return ( -
- -
- ); - } else if (childrenDataError) { - return ( - - ); - } else if (motherDataError) { - return ( - - ); - } - - return ( - <> - {[...childrenData, ...motherData]?.map(({ mother, motherAdmission, child, childAdmission }) => { - // patient A is the patient card's patient - const patientA = patient; - // patient B is either the mother or the child of patient A - const isPatientBTheMother = mother.uuid != patientA.uuid; - const patientB = isPatientBTheMother ? mother : child; - - // we display patient B here - const Icon = isPatientBTheMother ? MotherIcon : BabyIcon; - const patientBAdmission = isPatientBTheMother ? motherAdmission : childAdmission; - - return ( -
-
- -
-
- - - - -
-
- ); - })} - - ); -}; - -export default MotherChildRowExtension; diff --git a/packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-car-row.extension.tsx b/packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-row.component.tsx similarity index 77% rename from packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-car-row.extension.tsx rename to packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-row.component.tsx index 4cded2d6f..b9deae0f5 100644 --- a/packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-car-row.extension.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/card-rows/pending-items-row.component.tsx @@ -1,15 +1,19 @@ -import React, { useCallback, useEffect } from 'react'; -import { type WardPatientCard } from '../../types'; import { Hourglass } from '@carbon/react/icons'; +import React, { useCallback, useEffect } from 'react'; +import { type WardPatient } from '../../types'; -import { useConfig } from '@openmrs/esm-framework'; -import type { PendingItemsDefinition } from '../../config-schema'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; import { WardPatientPendingOrder } from '../row-elements/ward-patient-pending-order.component'; -import styles from '../ward-patient-card.scss'; import WardPatientPendingTransfer from '../row-elements/ward-patient-pending-transfer'; +import styles from '../ward-patient-card.scss'; + +export interface PendingItemsRowProps { + id: string; + wardPatient: WardPatient; +} -const PendingItemsCarRowExtension: WardPatientCard = (wardPatient) => { - const { orders, showPendingItems } = useConfig(); +const PendingItemsRow: React.FC = ({ id, wardPatient }) => { + const { orders, showPendingItems } = useElementConfig('pendingItems', id); const [hasPendingOrders, setHasPendingOrders] = React.useState(false); const hasPendingItems = !!wardPatient?.inpatientRequest || hasPendingOrders; @@ -47,4 +51,4 @@ const PendingItemsCarRowExtension: WardPatientCard = (wardPatient) => { ); }; -export default PendingItemsCarRowExtension; +export default PendingItemsRow; diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx index 98f5526b4..2db096a82 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx @@ -2,15 +2,15 @@ import { Tag } from '@carbon/react'; import { type OpenmrsResource, type Patient, type Visit } from '@openmrs/esm-framework'; import React, { type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { type ColoredObsTagsCardRowConfigObject } from '../../config-schema-extension-colored-obs-tags'; import { useObs } from '../../hooks/useObs'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; import styles from '../ward-patient-card.scss'; -import WardPatientSkeletonText from './ward-pateint-skeleton-text'; +import WardPatientSkeletonText from './ward-patient-skeleton-text'; import { getObsEncounterString, obsCustomRepresentation, useConceptToTagColorMap } from './ward-patient-obs.resource'; import WardPatientResponsiveTooltip from './ward-patient-responsive-tooltip'; interface WardPatientCodedObsTagsProps { - config: ColoredObsTagsCardRowConfigObject; + id: string; patient: Patient; visit: Visit; } @@ -25,9 +25,14 @@ interface WardPatientCodedObsTagsProps { * @param config * @returns */ -const WardPatientCodedObsTags: React.FC = ({ config, patient, visit }) => { - const { conceptUuid, summaryLabel, summaryLabelColor } = config; - const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation); +const WardPatientCodedObsTags: React.FC = ({ id, patient, visit }) => { + const config = useElementConfig('coloredObsTags', id); + const { conceptUuid, summaryLabel, summaryLabelColor } = config ?? {}; + const { data, isLoading } = useObs( + { patient: patient.uuid, concept: conceptUuid }, + conceptUuid != null, + obsCustomRepresentation, + ); const { t } = useTranslation(); const conceptToTagColorMap = useConceptToTagColorMap(config.tags); diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-header-address.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-header-address.tsx index 9812f629d..234336aaa 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-header-address.tsx @@ -1,15 +1,15 @@ -import React from 'react'; -import styles from '../ward-patient-card.scss'; import { type Patient } from '@openmrs/esm-framework'; -import { type AddressElementDefinition } from '../../config-schema'; +import React from 'react'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; export interface WardPatientAddressProps { patient: Patient; - config: AddressElementDefinition; + id: string; } -const WardPatientAddress: React.FC = ({ patient, config }) => { +const WardPatientAddress: React.FC = ({ patient, id }) => { const preferredAddress = patient?.person?.preferredAddress; + const config = useElementConfig("patientAddress", id); return ( <> diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-identifier.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-identifier.tsx index cb4fe7f11..a69c0b8b3 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-identifier.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { type IdentifierElementDefinition } from '../../config-schema'; +import { type IdentifierElementConfig } from '../../config-schema'; import { Tag } from '@carbon/react'; import { type Patient, translateFrom, type PatientIdentifier } from '@openmrs/esm-framework'; import { moduleName } from '../../constant'; import { useTranslation } from 'react-i18next'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; /** Sort the identifiers by preferred first. The identifier with value of true * takes precedence over false. If both identifiers have same preferred value, @@ -21,18 +22,18 @@ const identifierCompareFunction = (pi1: PatientIdentifier, pi2: PatientIdentifie export interface WardPatientIdentifierProps { patient: Patient; - /** If the config is not passed, this will be the default identifier element, which uses the preferred identifier type. */ - config?: IdentifierElementDefinition; + id?: string; } -const defaultConfig: IdentifierElementDefinition = { +const defaultConfig: IdentifierElementConfig = { id: 'patient-identifier', identifierTypeUuid: null, }; -const WardPatientIdentifier: React.FC = ({ config: configProp, patient }) => { +const WardPatientIdentifier: React.FC = ({ id, patient }) => { const { t } = useTranslation(); - const config = configProp ?? defaultConfig; + const config = useElementConfig('patientIdentifier', id) ?? defaultConfig; + const { identifierTypeUuid, label } = config; const patientIdentifiers = patient.identifiers.filter( (patientIdentifier: PatientIdentifier) => diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts index 4a96e90dc..5961bd331 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts @@ -1,7 +1,7 @@ import { restBaseUrl, useOpenmrsFetchAll, type Concept } from '@openmrs/esm-framework'; -import { type TagConfigObject } from '../../config-schema-extension-colored-obs-tags'; import { type Observation } from '../../types'; import { type TFunction } from 'i18next'; +import { type ColoredObsTagConfig } from '../../config-schema'; // prettier-ignore export const obsCustomRepresentation = @@ -13,7 +13,7 @@ export const obsCustomRepresentation = // get the setMembers of a concept set const conceptSetCustomRepresentation = 'custom:(uuid,setMembers:(uuid))'; -export function useConceptToTagColorMap(tags: Array) { +export function useConceptToTagColorMap(tags: Array = []) { // The TacConfigObject allows us to specify the mapping of // concept sets to colors. However, we also need to build a map of // concepts to colors. This function does that. diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.tsx index 546ba2622..95b21005b 100644 --- a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-obs.tsx @@ -1,23 +1,30 @@ -import { SkeletonText, Toggletip, ToggletipButton, ToggletipContent } from '@carbon/react'; -import { Information } from '@carbon/react/icons'; +import { SkeletonText } from '@carbon/react'; import { type OpenmrsResource, type Patient, type Visit } from '@openmrs/esm-framework'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { type ObsElementDefinition } from '../../config-schema'; +import { type ObsElementConfig } from '../../config-schema'; import { useObs } from '../../hooks/useObs'; import styles from '../ward-patient-card.scss'; import { getObsEncounterString, obsCustomRepresentation } from './ward-patient-obs.resource'; import WardPatientResponsiveTooltip from './ward-patient-responsive-tooltip'; +import { useElementConfig } from '../../ward-view/ward-view.resource'; export interface WardPatientObsProps { - config: ObsElementDefinition; + id: string; + configOverride?: ObsElementConfig; patient: Patient; visit: Visit; } -const WardPatientObs: React.FC = ({ config, patient, visit }) => { - const { conceptUuid, onlyWithinCurrentVisit, orderBy, limit, label } = config; - const { data, isLoading } = useObs({ patient: patient.uuid, concept: conceptUuid }, obsCustomRepresentation); +const WardPatientObs: React.FC = ({ id, configOverride, patient, visit }) => { + const config: ObsElementConfig = useElementConfig('obs', id); + const configToUse = configOverride ?? config; + const { conceptUuid, onlyWithinCurrentVisit, orderBy, limit, label } = configToUse ?? {}; + const { data, isLoading } = useObs( + { patient: patient.uuid, concept: conceptUuid }, + conceptUuid != null, + obsCustomRepresentation, + ); const { t } = useTranslation(); if (isLoading) { @@ -53,7 +60,7 @@ const WardPatientObs: React.FC = ({ config, patient, visit {labelToDisplay ? t('labelColon', '{{label}}:', { label: labelToDisplay }) : ''} - {obsNodes} +
{obsNodes}
); } else { diff --git a/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-pateint-skeleton-text.tsx b/packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-skeleton-text.tsx similarity index 100% rename from packages/esm-ward-app/src/ward-patient-card/row-elements/ward-pateint-skeleton-text.tsx rename to packages/esm-ward-app/src/ward-patient-card/row-elements/ward-patient-skeleton-text.tsx diff --git a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card-element.component.tsx b/packages/esm-ward-app/src/ward-patient-card/ward-patient-card-element.component.tsx deleted file mode 100644 index a673c352e..000000000 --- a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card-element.component.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { InlineNotification } from '@carbon/react'; -import { useConfig } from '@openmrs/esm-framework'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { type WardConfigObject } from '../config-schema'; -import { type WardPatient } from '../types'; -import WardPatientAge from './row-elements/ward-patient-age'; -import WardPatientAddress from './row-elements/ward-patient-header-address'; -import WardPatientIdentifier from './row-elements/ward-patient-identifier'; -import WardPatientObs from './row-elements/ward-patient-obs'; -import WardPatientTimeOnWard from './row-elements/ward-patient-time-on-ward'; -import WardPatientTimeSinceAdmission from './row-elements/ward-patient-time-since-admission'; -import WardPatientLocation from './row-elements/ward-patient-location'; - -export interface WardPatientCardElementProps extends WardPatient { - elementId: string; -} - -export const WardPatientCardElement: React.FC = ({ - elementId, - patient, - visit, - inpatientAdmission, -}) => { - const { obsElementDefinitions, identifierElementDefinitions, addressElementDefinitions } = - useConfig().wardPatientCards; - const { t } = useTranslation(); - const { encounterAssigningToCurrentInpatientLocation, firstAdmissionOrTransferEncounter } = inpatientAdmission ?? {}; - - switch (elementId) { - case 'patient-age': - return ; - case 'time-on-ward': { - return ( - - ); - } - case 'time-since-admission': { - return ; - } - case 'patient-location': { - return ; - } - default: { - const obsConfig = obsElementDefinitions.find((elementDef) => elementDef.id === elementId); - const idConfig = identifierElementDefinitions.find((elementDef) => elementDef.id === elementId); - const addressConfig = addressElementDefinitions.find((elementDef) => elementDef.id === elementId); - if (obsConfig) { - return ; - } else if (idConfig) { - return ; - } else if (addressConfig) { - return ; - } else { - return ( - - {t( - 'invalidElementIdCopy', - 'The configuration provided is invalid. It contains the following unknown element ID:', - )}{' '} - {elementId} - - ); - } - } - } -}; diff --git a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx b/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx index 9efc44045..0916a5f4a 100644 --- a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx @@ -1,64 +1,28 @@ -import { ExtensionSlot, getPatientName, launchWorkspace } from '@openmrs/esm-framework'; -import classNames from 'classnames'; -import React from 'react'; -import { useCurrentWardCardConfig } from '../hooks/useCurrentWardCardConfig'; -import { type WardPatientCard, type WardPatientWorkspaceProps } from '../types'; -import WardPatientBedNumber from './row-elements/ward-patient-bed-number'; -import WardPatientName from './row-elements/ward-patient-name'; -import { WardPatientCardElement } from './ward-patient-card-element.component'; +import { getPatientName, useAppContext } from '@openmrs/esm-framework'; +import React, { type ReactNode } from 'react'; +import { type WardViewContext, type WardPatient } from '../types'; import styles from './ward-patient-card.scss'; -import { launchPatientWorkspace, setWardPatient } from './ward-patient-resource'; +import { launchPatientWorkspace, setPatientWorkspaceProps } from './ward-patient-resource'; -const WardPatientCard: WardPatientCard = (wardPatient) => { - const { patient, bed } = wardPatient; - const { id, headerRowElements, footerRowElements } = useCurrentWardCardConfig(); +interface Props { + children: ReactNode; + wardPatient: WardPatient; +} - const headerExtensionSlotName = - id == 'default' ? 'ward-patient-card-header-slot' : `ward-patient-card-header-${id}-slot`; - const rowsExtensionSlotName = id == 'default' ? 'ward-patient-card-slot' : `ward-patient-card-${id}-slot`; - const footerExtensionSlotName = - id == 'default' ? 'ward-patient-card-footer-slot' : `ward-patient-card-footer-${id}-slot`; +const WardPatientCard: React.FC = ({ children, wardPatient }) => { + const { patient } = wardPatient; + const { WardPatientHeader } = useAppContext('ward-view-context') ?? {}; return (
-
- {bed ? : null} - - {headerRowElements.map((elementId, i) => ( - - ))} - -
- {footerRowElements.length > 0 && ( -
- {footerRowElements.map((elementId, i) => ( - - ))} - -
- )} - - + {children}
]} />); expect(screen.getByText('1 admission request')).toBeInTheDocument(); }); diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx index 87005bab2..5255b4c4e 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.component.tsx @@ -1,32 +1,29 @@ -import React from 'react'; -import styles from './ward-metrics.scss'; -import { useBeds } from '../hooks/useBeds'; import { showNotification, useAppContext, useFeatureFlag } from '@openmrs/esm-framework'; +import React from 'react'; import { useTranslation } from 'react-i18next'; +import type { WardViewContext } from '../types'; import { getWardMetricNameTranslation, getWardMetrics, getWardMetricValueTranslation, } from '../ward-view/ward-view.resource'; import WardMetric from './ward-metric.component'; -import type { WardPatientGroupDetails } from '../types'; -import useWardLocation from '../hooks/useWardLocation'; +import styles from './ward-metrics.scss'; const wardMetrics = [{ name: 'patients' }, { name: 'freeBeds' }, { name: 'capacity' }]; const WardMetrics = () => { - const { location } = useWardLocation(); const { t } = useTranslation(); const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); - const wardPatientGroup = useAppContext('ward-patients-group'); + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; const { admissionLocationResponse, inpatientAdmissionResponse, inpatientRequestResponse, bedLayouts } = - wardPatientGroup || {}; + wardPatientGroupDetails || {}; const { isLoading, error } = admissionLocationResponse ?? {}; const isDataLoading = admissionLocationResponse?.isLoading || inpatientAdmissionResponse?.isLoading || inpatientRequestResponse?.isLoading; - if (!wardPatientGroup) return <>; + if (!wardPatientGroupDetails) return <>; if (error) { showNotification({ @@ -36,7 +33,7 @@ const WardMetrics = () => { }); } - const wardMetricValues = getWardMetrics(bedLayouts, wardPatientGroup); + const wardMetricValues = getWardMetrics(bedLayouts, wardPatientGroupDetails); return (
{isBedManagementModuleInstalled ? ( @@ -64,7 +61,11 @@ const WardMetrics = () => { metricValue={ error ? '--' - : getWardMetricValueTranslation('pendingOut', t, wardPatientGroup?.wardPatientPendingCount?.toString()) + : getWardMetricValueTranslation( + 'pendingOut', + t, + wardPatientGroupDetails?.wardPatientPendingCount?.toString(), + ) } isLoading={!!isDataLoading} key="pending" diff --git a/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx index eff3d3a37..3991e9075 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx +++ b/packages/esm-ward-app/src/ward-view-header/ward-metrics.test.tsx @@ -12,6 +12,8 @@ import { useInpatientAdmission } from '../hooks/useInpatientAdmission'; import useWardLocation from '../hooks/useWardLocation'; import { screen } from '@testing-library/react'; import { useAppContext } from '@openmrs/esm-framework'; +import { type WardViewContext } from '../types'; +import { mockWardViewContext } from '../../mock'; const wardMetrics = [ { name: 'patients', key: 'patients', defaultTranslation: 'Patients' }, @@ -19,67 +21,11 @@ const wardMetrics = [ { name: 'capacity', key: 'capacity', defaultTranslation: 'Capacity' }, ]; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn().mockReturnValue({}), -})); +jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); -jest.mock('../hooks/useWardLocation', () => - jest.fn().mockReturnValue({ - location: { uuid: 'abcd', display: 'mock location' }, - isLoadingLocation: false, - errorFetchingLocation: null, - invalidLocation: false, - }), -); - -jest.mock('../hooks/useBeds', () => ({ - useBeds: jest.fn(), -})); - -jest.mock('../hooks/useAdmissionLocation', () => ({ - useAdmissionLocation: jest.fn(), -})); -jest.mock('../hooks/useInpatientAdmission', () => ({ - useInpatientAdmission: jest.fn(), -})); - -jest.mock('../hooks/useInpatientRequest', () => ({ - useInpatientRequest: jest.fn(), -})); - -const mockUseWardLocation = jest.mocked(useWardLocation); - -const mockAdmissionLocationResponse = jest.mocked(useAdmissionLocation).mockReturnValue({ - error: undefined, - mutate: jest.fn(), - isValidating: false, - isLoading: false, - admissionLocation: mockAdmissionLocation, -}); -const mockInpatientAdmissionResponse = jest.mocked(useInpatientAdmission).mockReturnValue({ - error: undefined, - mutate: jest.fn(), - isValidating: false, - isLoading: false, - inpatientAdmissions: mockInpatientAdmissions, -}); - -const inpatientAdmissionsUuidMap = getInpatientAdmissionsUuidMap(mockInpatientAdmissions); -const mockWardPatientGroupDetails = { - admissionLocationResponse: mockAdmissionLocationResponse(), - inpatientAdmissionResponse: mockInpatientAdmissionResponse(), - ...createAndGetWardPatientGrouping(mockInpatientAdmissions, mockAdmissionLocation, mockInpatientRequest), -}; -jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails); describe('Ward Metrics', () => { it('Should display metrics of in the ward ', () => { - mockUseWardLocation.mockReturnValueOnce({ - location: null, - isLoadingLocation: false, - errorFetchingLocation: null, - invalidLocation: true, - }); + const mockWardPatientGroupDetails = mockWardViewContext.wardPatientGroupDetails; const { bedLayouts } = mockWardPatientGroupDetails; const bedMetrics = getWardMetrics(bedLayouts, mockWardPatientGroupDetails); renderWithSwr(); diff --git a/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx b/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx index 74bd3f8a0..75691bcba 100644 --- a/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx +++ b/packages/esm-ward-app/src/ward-view-header/ward-view-header.component.tsx @@ -1,19 +1,21 @@ -import React from 'react'; +import React, { type ReactNode } from 'react'; import styles from './ward-view-header.scss'; import AdmissionRequestsBar from './admission-requests-bar.component'; import useWardLocation from '../hooks/useWardLocation'; import WardMetrics from './ward-metrics.component'; -interface WardViewHeaderProps {} +interface WardViewHeaderProps { + wardPendingPatients: ReactNode; +} -const WardViewHeader: React.FC = () => { +const WardViewHeader: React.FC = ({ wardPendingPatients }) => { const { location } = useWardLocation(); return (

{location?.display}

- +
); }; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-beds.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-beds.component.tsx new file mode 100644 index 000000000..1bd5cd60d --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-beds.component.tsx @@ -0,0 +1,42 @@ +import { useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import WardBed from '../../beds/ward-bed.component'; +import { type WardPatient, type WardViewContext } from '../../types'; +import { bedLayoutToBed } from '../ward-view.resource'; +import DefaultWardPatientCard from './default-ward-patient-card.component'; + +function DefaultWardBeds() { + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { bedLayouts, wardAdmittedPatientsWithBed } = wardPatientGroupDetails ?? {}; + + const wardBeds = bedLayouts?.map((bedLayout) => { + const { patients } = bedLayout; + const bed = bedLayoutToBed(bedLayout); + const wardPatients: WardPatient[] = patients.map((patient): WardPatient => { + const inpatientAdmission = wardAdmittedPatientsWithBed?.get(patient.uuid); + if (inpatientAdmission) { + const { patient, visit, currentInpatientRequest } = inpatientAdmission; + return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; + } else { + // for some reason this patient is in a bed but not in the list of admitted patients, so we need to use the patient data from the bed endpoint + return { + patient: patient, + visit: null, + bed, + inpatientAdmission: null, // populate after BED-13 + inpatientRequest: null, + }; + } + }); + const patientCards = wardPatients.map(wardPatient => ( + + )); + return ; + }); + + return <>{wardBeds}; +} + +export default DefaultWardBeds; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx new file mode 100644 index 000000000..a2c0c3952 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card-header.component.tsx @@ -0,0 +1,32 @@ +import classNames from 'classnames'; +import React from 'react'; +import WardPatientAge from '../../ward-patient-card/row-elements/ward-patient-age'; +import WardPatientBedNumber from '../../ward-patient-card/row-elements/ward-patient-bed-number'; +import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-patient-identifier'; +import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name'; +import WardPatientTimeOnWard from '../../ward-patient-card/row-elements/ward-patient-time-on-ward'; +import WardPatientTimeSinceAdmission from '../../ward-patient-card/row-elements/ward-patient-time-since-admission'; +import styles from '../../ward-patient-card/ward-patient-card.scss'; +import { type WardPatientCardType } from '../../types'; +import WardPatientGender from '../../ward-patient-card/row-elements/ward-patient-gender.component'; + +const DefaultWardPatientCardHeader: WardPatientCardType = (wardPatient) => { + const { patient, bed, inpatientAdmission } = wardPatient; + const { encounterAssigningToCurrentInpatientLocation, firstAdmissionOrTransferEncounter } = inpatientAdmission ?? {}; + + return ( +
+ {bed ? : null} + + + + + + +
+ ); +}; + +export default DefaultWardPatientCardHeader; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card.component.tsx new file mode 100644 index 000000000..bbe129849 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-patient-card.component.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { type WardPatientCardType } from '../../types'; +import AdmissionRequestNoteRow from '../../ward-patient-card/card-rows/admission-request-note-row.component'; +import PendingItemsRow from '../../ward-patient-card/card-rows/pending-items-row.component'; +import WardPatientCard from '../../ward-patient-card/ward-patient-card.component'; +import styles from '../../ward-patient-card/ward-patient-card.scss'; +import DefaultWardPatientCardHeader from './default-ward-patient-card-header.component'; + +const DefaultWardPatientCard: WardPatientCardType = (wardPatient) => { + const { bed } = wardPatient; + + const card = ( + + + + + + ); + + if (bed) { + return card; + } else { + return ( +
+
{card}
+
+ ); + } +}; + +export default DefaultWardPatientCard; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-pending-patients.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-pending-patients.component.tsx new file mode 100644 index 000000000..5d0513f68 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-pending-patients.component.tsx @@ -0,0 +1,52 @@ +import { ErrorState, useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { type WardViewContext, type InpatientRequest } from '../../types'; +import AdmissionRequestCard from '../../ward-workspace/admission-request-card/admission-request-card.component'; +import WardPatientSkeletonText from '../../ward-patient-card/row-elements/ward-patient-skeleton-text'; +import AdmissionRequestNoteRow from '../../ward-patient-card/card-rows/admission-request-note-row.component'; + +function DefaultWardPendingPatients() { + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { t } = useTranslation(); + const { inpatientRequestResponse } = wardPatientGroupDetails ?? {}; + const { + inpatientRequests, + isLoading: isLoadingInpatientRequests, + error: errorFetchingInpatientRequests, + } = inpatientRequestResponse ?? {}; + + return isLoadingInpatientRequests ? ( + + ) : errorFetchingInpatientRequests ? ( + + ) : ( + <> + {inpatientRequests?.map((request: InpatientRequest, i) => { + const wardPatient = { + patient: request.patient, + visit: request.visit, + bed: null, + inpatientRequest: request, + inpatientAdmission: null, + }; + + return ( + + + + ); + })} + + ); +} + +export default DefaultWardPendingPatients; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx new file mode 100644 index 000000000..41c4b8667 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-unassigned-patients.component.tsx @@ -0,0 +1,32 @@ +import { useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { type WardViewContext } from '../../types'; +import DefaultWardPatientCard from './default-ward-patient-card.component'; + +/** + * Renders a list of patients in the ward that are admitted but not assigned a bed + * @returns + */ +function DefaultWardUnassignedPatients() { + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { wardUnassignedPatientsList } = wardPatientGroupDetails ?? {}; + + const wardUnassignedPatients = wardUnassignedPatientsList?.map((inpatientAdmission) => { + return ( + + ); + }); + + return <>{wardUnassignedPatients}; +} + +export default DefaultWardUnassignedPatients; diff --git a/packages/esm-ward-app/src/ward-view/default-ward/default-ward-view.component.tsx b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-view.component.tsx new file mode 100644 index 000000000..ee73e2339 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/default-ward/default-ward-view.component.tsx @@ -0,0 +1,31 @@ +import { useDefineAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { useWardPatientGrouping } from '../../hooks/useWardPatientGrouping'; +import { type WardViewContext } from '../../types'; +import WardViewHeader from '../../ward-view-header/ward-view-header.component'; +import Ward from '../ward.component'; +import DefaultWardBeds from './default-ward-beds.component'; +import DefaultWardPendingPatients from './default-ward-pending-patients.component'; +import DefaultWardUnassignedPatients from './default-ward-unassigned-patients.component'; +import DefaultWardPatientCardHeader from './default-ward-patient-card-header.component'; + +const DefaultWardView = () => { + const wardPatientGroupDetails = useWardPatientGrouping(); + useDefineAppContext('ward-view-context', { + wardPatientGroupDetails, + WardPatientHeader: DefaultWardPatientCardHeader + }); + + const wardBeds = ; + const wardUnassignedPatients = ; + const wardPendingPatients = ; + + return ( + <> + + + + ); +}; + +export default DefaultWardView; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-beds.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-beds.component.tsx new file mode 100644 index 000000000..1a6d6843c --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-beds.component.tsx @@ -0,0 +1,60 @@ +import { useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import WardBed from '../../beds/ward-bed.component'; +import { type MotherAndChild, type WardPatient, type WardViewContext } from '../../types'; +import { bedLayoutToBed } from '../ward-view.resource'; +import MaternalWardPatientCard from './maternal-ward-patient-card.component'; + +interface MaternalWardBedsProps { + motherChildrenRelationshipsByPatient: Map; +} + +const MaternalWardBeds: React.FC = ({ motherChildrenRelationshipsByPatient }) => { + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { bedLayouts, wardAdmittedPatientsWithBed } = wardPatientGroupDetails ?? {}; + + const wardBeds = bedLayouts?.map((bedLayout) => { + const { patients: patientsInCurrentBed } = bedLayout; + const bed = bedLayoutToBed(bedLayout); + const wardPatients: WardPatient[] = patientsInCurrentBed + .map((patient): WardPatient => { + const inpatientAdmission = wardAdmittedPatientsWithBed?.get(patient.uuid); + if (inpatientAdmission) { + const { patient, visit, currentInpatientRequest } = inpatientAdmission; + return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; + } else { + // for some reason this patient is in a bed but not in the list of admitted patients, + // so we need to use the patient data from the bed endpoint + return { + patient: patient, + visit: null, + bed, + inpatientAdmission: null, + inpatientRequest: null, + }; + } + }) + .filter((wardPatient) => { + // filter out any child patient whose mother is also assigned to the same bed + // (the child patient will instead have a sub-card rendered in the mother's patient card) + const patientUuid = wardPatient.patient.uuid; + for (const relationship of motherChildrenRelationshipsByPatient?.get(patientUuid) ?? []) { + if (relationship.child.uuid == patientUuid) { + if (patientsInCurrentBed.some((patient) => patient.uuid == relationship.mother.uuid)) { + return false; + } + } + } + return true; + }); + const patientCards = wardPatients.map((wardPatient) => ( + + )); + + return ; + }); + + return <>{wardBeds}; +}; + +export default MaternalWardBeds; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx new file mode 100644 index 000000000..f55fb2961 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card-header.component.tsx @@ -0,0 +1,30 @@ +import classNames from 'classnames'; +import React from 'react'; +import { type WardPatientCardType } from '../../types'; +import WardPatientAge from '../../ward-patient-card/row-elements/ward-patient-age'; +import WardPatientBedNumber from '../../ward-patient-card/row-elements/ward-patient-bed-number'; +import WardPatientAddress from '../../ward-patient-card/row-elements/ward-patient-header-address'; +import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-patient-identifier'; +import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name'; +import WardPatientObs from '../../ward-patient-card/row-elements/ward-patient-obs'; +import WardPatientTimeSinceAdmission from '../../ward-patient-card/row-elements/ward-patient-time-since-admission'; +import styles from '../../ward-patient-card/ward-patient-card.scss'; + +const MaternalWardPatientCardHeader: WardPatientCardType = (wardPatient) => { + const { patient, bed, visit, inpatientAdmission } = wardPatient; + const { firstAdmissionOrTransferEncounter } = inpatientAdmission ?? {}; + + return ( +
+ {bed ? : null} + + + + + + +
+ ); +}; + +export default MaternalWardPatientCardHeader; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx new file mode 100644 index 000000000..c5740f9a4 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-patient-card.component.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +import React from 'react'; +import { type WardPatientCardType } from '../../types'; +import AdmissionRequestNoteRow from '../../ward-patient-card/card-rows/admission-request-note-row.component'; +import CodedObsTagsRow from '../../ward-patient-card/card-rows/coded-obs-tags-row.component'; +import MotherChildRowExtension from '../../ward-patient-card/card-rows/mother-child-row.component'; +import PendingItemsRow from '../../ward-patient-card/card-rows/pending-items-row.component'; +import WardPatientObs from '../../ward-patient-card/row-elements/ward-patient-obs'; +import WardPatientTimeOnWard from '../../ward-patient-card/row-elements/ward-patient-time-on-ward'; +import WardPatientCard from '../../ward-patient-card/ward-patient-card.component'; +import styles from '../../ward-patient-card/ward-patient-card.scss'; +import MaternalWardPatientCardHeader from './maternal-ward-patient-card-header.component'; + +const MaternalWardPatientCard: WardPatientCardType = (wardPatient) => { + const { patient, visit, bed, inpatientAdmission } = wardPatient; + const { encounterAssigningToCurrentInpatientLocation } = inpatientAdmission ?? {}; + + const card = ( + + +
+ + +
+ + + +
+ ); + + if (bed) { + return card; + } else { + return ( +
+
{card}
+
+ ); + } +}; + +export default MaternalWardPatientCard; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx new file mode 100644 index 000000000..6005fe671 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-pending-patients.component.tsx @@ -0,0 +1,48 @@ +import { ErrorState, useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { type InpatientRequest, type WardViewContext } from '../../types'; +import AdmissionRequestNoteRow from '../../ward-patient-card/card-rows/admission-request-note-row.component'; +import CodedObsTagsRow from '../../ward-patient-card/card-rows/coded-obs-tags-row.component'; +import MotherChildRowExtension from '../../ward-patient-card/card-rows/mother-child-row.component'; +import WardPatientSkeletonText from '../../ward-patient-card/row-elements/ward-patient-skeleton-text'; +import AdmissionRequestCard from '../../ward-workspace/admission-request-card/admission-request-card.component'; + +function MaternalWardPendingPatients() { + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { t } = useTranslation(); + const { inpatientRequestResponse } = wardPatientGroupDetails ?? {}; + const { + inpatientRequests, + isLoading: isLoadingInpatientRequests, + error: errorFetchingInpatientRequests, + } = inpatientRequestResponse ?? {}; + + return isLoadingInpatientRequests ? ( + + ) : errorFetchingInpatientRequests ? ( + + ) : ( + <> + {inpatientRequests?.map((request: InpatientRequest, i) => { + const wardPatient = { + patient: request.patient, + visit: request.visit, + bed: null, + inpatientRequest: request, + inpatientAdmission: null, + }; + + return ( + + + + + + ); + })} + + ); +} + +export default MaternalWardPendingPatients; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx new file mode 100644 index 000000000..03a4c8538 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-unassigned-patients.component.tsx @@ -0,0 +1,32 @@ +import { useAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { type WardViewContext } from '../../types'; +import MaternalWardPatientCard from './maternal-ward-patient-card.component'; + +/** + * Renders a list of patients in the ward that are admitted but not assigned a bed + * @returns + */ +function MaternalWardUnassignedPatients() { + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { wardUnassignedPatientsList } = wardPatientGroupDetails ?? {}; + + const wardUnassignedPatients = wardUnassignedPatientsList?.map((inpatientAdmission) => { + return ( + + ); + }); + + return <>{wardUnassignedPatients}; +} + +export default MaternalWardUnassignedPatients; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.component.tsx b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.component.tsx new file mode 100644 index 000000000..aaada99f8 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.component.tsx @@ -0,0 +1,41 @@ +import { useDefineAppContext } from '@openmrs/esm-framework'; +import React from 'react'; +import { useWardPatientGrouping } from '../../hooks/useWardPatientGrouping'; +import { type MaternalWardViewContext, type WardViewContext } from '../../types'; +import WardViewHeader from '../../ward-view-header/ward-view-header.component'; +import Ward from '../ward.component'; +import MaternalWardBeds from './maternal-ward-beds.component'; +import MaternalWardPatientCardHeader from './maternal-ward-patient-card-header.component'; +import MaternalWardPendingPatients from './maternal-ward-pending-patients.component'; +import MaternalWardUnassignedPatients from './maternal-ward-unassigned-patients.component'; +import { useMotherChildrenRelationshipsByPatient } from './maternal-ward-view.resource'; + +const MaternalWardView = () => { + const wardPatientGroupDetails = useWardPatientGrouping(); + useDefineAppContext('ward-view-context', { + wardPatientGroupDetails, + WardPatientHeader: MaternalWardPatientCardHeader, + }); + const { allWardPatientUuids, isLoading } = wardPatientGroupDetails; + + const motherChildrenRelationshipsByPatient = useMotherChildrenRelationshipsByPatient( + Array.from(allWardPatientUuids), + !isLoading, + ); + useDefineAppContext('maternal-ward-view-context', { + motherChildrenRelationshipsByPatient, + }); + + const wardBeds = ; + const wardUnassignedPatients = ; + const wardPendingPatients = ; + + return ( + <> + + + + ); +}; + +export default MaternalWardView; diff --git a/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.resource.ts b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.resource.ts new file mode 100644 index 000000000..be4bc8393 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/materal-ward/maternal-ward-view.resource.ts @@ -0,0 +1,75 @@ +import { useMemo } from 'react'; +import { type MothersAndChildrenSearchCriteria, useMotherAndChildren } from '../../hooks/useMotherAndChildren'; +import { type MotherAndChild } from '../../types'; +import { showNotification } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; + +const motherAndChildrenRep = + 'custom:(childAdmission,mother:(person,identifiers:full,uuid),child:(person,identifiers:full,uuid),motherAdmission)'; + +export function useMotherChildrenRelationshipsByPatient(allWardPatientUuids: string[], fetch: boolean) { + const { t } = useTranslation(); + + const getChildrenRequestParams: MothersAndChildrenSearchCriteria = { + mothers: allWardPatientUuids, + requireMotherHasActiveVisit: true, + requireChildHasActiveVisit: true, + requireChildBornDuringMothersActiveVisit: true, + }; + + const getMotherRequestParams: MothersAndChildrenSearchCriteria = { + children: allWardPatientUuids, + requireMotherHasActiveVisit: true, + requireChildHasActiveVisit: true, + requireChildBornDuringMothersActiveVisit: true, + }; + + const { + data: childrenData, + isLoading: isLoadingChildrenData, + error: childrenDataError, + } = useMotherAndChildren(getChildrenRequestParams, fetch && allWardPatientUuids.length > 0, motherAndChildrenRep); + const { + data: motherData, + isLoading: isLoadingMotherData, + error: motherDataError, + } = useMotherAndChildren(getMotherRequestParams, fetch && allWardPatientUuids.length > 0, motherAndChildrenRep); + + if (childrenDataError) { + showNotification({ + title: t('errorLoadingChildren', 'Error loading children info'), + kind: 'error', + critical: true, + description: childrenDataError?.message, + }); + } + if (motherDataError) { + showNotification({ + title: t('errorLoadingMother', 'Error loading mother info'), + kind: 'error', + critical: true, + description: motherDataError?.message, + }); + } + + const motherChildrenRelationshipsByPatient = useMemo(() => { + if (childrenData != null && motherData != null) { + const map = new Map(); + for (const data of [...childrenData, ...motherData]) { + if (!map.has(data.child.uuid)) { + map.set(data.child.uuid, []); + } + if (!map.has(data.mother.uuid)) { + map.set(data.mother.uuid, []); + } + map.get(data.child.uuid).push(data); + map.get(data.mother.uuid).push(data); + } + return map; + } else { + return null; + } + }, [childrenData, motherData, isLoadingChildrenData, isLoadingMotherData]); + + return motherChildrenRelationshipsByPatient; +} diff --git a/packages/esm-ward-app/src/ward-view/ward-bed.component.tsx b/packages/esm-ward-app/src/ward-view/ward-bed.component.tsx deleted file mode 100644 index 1ad7f0f98..000000000 --- a/packages/esm-ward-app/src/ward-view/ward-bed.component.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import EmptyBed from '../beds/empty-bed.component'; -import { type WardPatient, type Bed } from '../types'; -import OccupiedBed from '../beds/occupied-bed.component'; -export interface WardBedProps { - wardPatients: Array; - bed: Bed; -} - -const WardBed: React.FC = ({ bed, wardPatients }) => { - return wardPatients?.length > 0 ? : ; -}; - -export default WardBed; diff --git a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx index 9a74c1dfd..609f6f4f3 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.component.tsx +++ b/packages/esm-ward-app/src/ward-view/ward-view.component.tsx @@ -1,26 +1,20 @@ import { InlineNotification } from '@carbon/react'; -import { useAppContext, useDefineAppContext, useFeatureFlag, WorkspaceContainer } from '@openmrs/esm-framework'; -import React, { useEffect, useRef } from 'react'; +import { ExtensionSlot, useFeatureFlag } from '@openmrs/esm-framework'; +import classNames from 'classnames'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import EmptyBedSkeleton from '../beds/empty-bed-skeleton'; -import UnassignedPatient from '../beds/unassigned-patient.component'; import useWardLocation from '../hooks/useWardLocation'; -import { useWardPatientGrouping } from '../hooks/useWardPatientGrouping'; -import { type WardPatient, type WardPatientGroupDetails } from '../types'; -import WardViewHeader from '../ward-view-header/ward-view-header.component'; -import WardBed from './ward-bed.component'; -import { bedLayoutToBed } from './ward-view.resource'; +import { useWardConfig } from './ward-view.resource'; import styles from './ward-view.scss'; -import classNames from 'classnames'; -const WardView = () => { +const WardView: React.FC<{}> = () => { const response = useWardLocation(); - const { isLoadingLocation, invalidLocation } = response; + const { isLoadingLocation, invalidLocation, location } = response; const { t } = useTranslation(); - const wardPatientsGroupDetails = useWardPatientGrouping(); - useDefineAppContext('ward-patients-group', wardPatientsGroupDetails); + const locationUuid = location?.uuid; const isVertical = useFeatureFlag('ward-view-vertical-tiling'); + const wardConfig = useWardConfig(locationUuid); if (isLoadingLocation) { return <>; @@ -30,147 +24,13 @@ const WardView = () => { return ; } - return ( - <> -
- - -
- - - ); -}; - -const WardViewMain = () => { - const { location } = useWardLocation(); - const { t } = useTranslation(); - const isVertical = useFeatureFlag('ward-view-vertical-tiling'); - - const wardPatientsGrouping = useAppContext('ward-patients-group'); - const { bedLayouts, wardAdmittedPatientsWithBed, wardUnassignedPatientsList } = wardPatientsGrouping ?? {}; - const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } = - wardPatientsGrouping?.admissionLocationResponse ?? {}; - const { - isLoading: isLoadingInpatientAdmissions, - error: errorLoadingInpatientAdmissions, - hasMore: hasMoreInpatientAdmissions, - loadMore: loadMoreInpatientAdmissions, - } = wardPatientsGrouping?.inpatientAdmissionResponse ?? {}; - const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); - - const scrollToLoadMoreTrigger = useRef(null); - useEffect( - function scrollToLoadMore() { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - if (hasMoreInpatientAdmissions && !errorLoadingInpatientAdmissions && !isLoadingInpatientAdmissions) { - loadMoreInpatientAdmissions(); - } - } - }); - }, - { threshold: 1 }, - ); - - if (scrollToLoadMoreTrigger.current) { - observer.observe(scrollToLoadMoreTrigger.current); - } - return () => { - if (scrollToLoadMoreTrigger.current) { - observer.unobserve(scrollToLoadMoreTrigger.current); - } - }; - }, - [scrollToLoadMoreTrigger, hasMoreInpatientAdmissions, errorLoadingInpatientAdmissions, loadMoreInpatientAdmissions], - ); - - if (!wardPatientsGrouping) return <>; - - const wardBeds = bedLayouts?.map((bedLayout) => { - const { patients } = bedLayout; - const bed = bedLayoutToBed(bedLayout); - const wardPatients: WardPatient[] = patients.map((patient): WardPatient => { - const inpatientAdmission = wardAdmittedPatientsWithBed.get(patient.uuid); - if (inpatientAdmission) { - const { patient, visit, currentInpatientRequest } = inpatientAdmission; - return { patient, visit, bed, inpatientAdmission, inpatientRequest: currentInpatientRequest || null }; - } else { - // for some reason this patient is in a bed but not in the list of admitted patients, so we need to use the patient data from the bed endpoint - return { - patient: patient, - visit: null, - bed, - inpatientAdmission: null, // populate after BED-13 - inpatientRequest: null, - }; - } - }); - return ; - }); - - const wardUnassignedPatients = wardUnassignedPatientsList?.map((inpatientAdmission) => { - return ( - - ); - }); + const wardId = wardConfig.id; return ( -
- {wardBeds} - {bedLayouts?.length == 0 && isBedManagementModuleInstalled && ( - - )} - {wardUnassignedPatients} - {(isLoadingAdmissionLocation || isLoadingInpatientAdmissions) && } - {errorLoadingAdmissionLocation && ( - - )} - {errorLoadingInpatientAdmissions && ( - - )} -
+
+
); }; -const EmptyBeds = () => { - return ( - <> - {Array(20) - .fill(0) - .map((_, i) => ( - - ))} - - ); -}; - export default WardView; diff --git a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts index 970cad372..a7311392a 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.resource.ts +++ b/packages/esm-ward-app/src/ward-view/ward-view.resource.ts @@ -1,4 +1,16 @@ -import { type Patient } from '@openmrs/esm-framework'; +import { showNotification, useConfig, type Patient } from '@openmrs/esm-framework'; +import type { TFunction } from 'i18next'; +import { useMemo } from 'react'; +import { + type PendingItemsElementConfig, + type ColoredObsTagsElementConfig, + type IdentifierElementConfig, + type ObsElementConfig, + type PatientAddressElementConfig, + type WardConfigObject, + type WardDefinition, + type AdmissionRequestNoteElementConfig, +} from '../config-schema'; import type { AdmissionLocationFetchResponse, Bed, @@ -8,7 +20,7 @@ import type { WardMetrics, WardPatientGroupDetails, } from '../types'; -import type { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; // the server side has 2 slightly incompatible types for Bed export function bedLayoutToBed(bedLayout: BedLayout): Bed { @@ -34,7 +46,6 @@ export function filterBeds(admissionLocation: AdmissionLocationFetchResponse): B return bedLayouts; } -//TODO: This implementation will change when the api is ready export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPatientGroupDetails): WardMetrics { const bedMetrics = { patients: '--', @@ -43,7 +54,7 @@ export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPa }; if (bedLayouts == null || bedLayouts.length == 0) return bedMetrics; const total = bedLayouts.length; - const occupiedBeds = bedLayouts.filter((bed) => bed.patients.length>0); + const occupiedBeds = bedLayouts.filter((bed) => bed.patients.length > 0); const patients = occupiedBeds.length; const freeBeds = total - patients; const capacity = total != 0 ? Math.trunc((wardPatientGroup.totalPatientsCount / total) * 100) : 0; @@ -57,7 +68,10 @@ export function getWardMetrics(bedLayouts: BedLayout[], wardPatientGroup: WardPa export function getInpatientAdmissionsUuidMap(inpatientAdmissions: InpatientAdmission[]) { const map = new Map(); for (const inpatientAdmission of inpatientAdmissions ?? []) { - map.set(inpatientAdmission.patient.uuid, inpatientAdmission); + // TODO: inpatientAdmission is undefined sometimes, why? + if (inpatientAdmission) { + map.set(inpatientAdmission.patient.uuid, inpatientAdmission); + } } return map; } @@ -103,7 +117,10 @@ export function createAndGetWardPatientGrouping( const totalPatientsCount = allWardPatientUuids.size; for (const inpatientRequest of inpatientRequests ?? []) { - allWardPatientUuids.add(inpatientRequest.patient.uuid); + // TODO: inpatientRequest is undefined sometimes, why? + if (inpatientRequest) { + allWardPatientUuids.add(inpatientRequest.patient.uuid); + } } return { @@ -142,3 +159,58 @@ export function getWardMetricValueTranslation(name: string, t: TFunction, value: return t('pendingOutMetricValue', '{{ metricValue }}', { metricValue: value }); } } + +export function useElementConfig(elementType: 'obs', id: string): ObsElementConfig; +export function useElementConfig(elementType: 'patientIdentifier', id: string): IdentifierElementConfig; +export function useElementConfig(elementType: 'patientAddress', id: string): PatientAddressElementConfig; +export function useElementConfig(elementType: 'coloredObsTags', id: string): ColoredObsTagsElementConfig; +export function useElementConfig(elementType: 'pendingItems', id: string): PendingItemsElementConfig; +export function useElementConfig(elementType: 'admissionRequestNote', id: string): AdmissionRequestNoteElementConfig; +export function useElementConfig(elementType, id: string): object { + const config = useConfig(); + const { t } = useTranslation(); + + try { + return config?.patientCardElements?.[elementType]?.find((elementConfig) => elementConfig?.id == id); + } catch (e) { + showNotification({ + title: t('errorConfiguringPatientCard', 'Error configuring patient card'), + kind: 'error', + critical: true, + description: t( + 'errorConfiguringPatientCardMessage', + 'Unable to find configuration for {{elementType}}, id: {{id}}', + { + elementType, + id, + }, + ), + }); + return null; + } +} + +export function useWardConfig(locationUuid: string): WardDefinition { + const { wards } = useConfig(); + + const currentWardConfig = useMemo(() => { + const cardDefinition = wards?.find((wardDef) => { + return ( + wardDef.appliedTo == null || + wardDef.appliedTo?.length == 0 || + wardDef.appliedTo.some((criteria) => criteria.location == locationUuid) + ); + }); + + return cardDefinition; + }, [wards, locationUuid]); + + if (!currentWardConfig) { + console.warn( + 'No ward card configuration has `appliedTo` criteria that matches the current location. Using the default configuration.', + ); + return { id: 'default-ward' }; + } + + return currentWardConfig; +} diff --git a/packages/esm-ward-app/src/ward-view/ward-view.test.tsx b/packages/esm-ward-app/src/ward-view/ward-view.test.tsx index c95835af8..9c97eb943 100644 --- a/packages/esm-ward-app/src/ward-view/ward-view.test.tsx +++ b/packages/esm-ward-app/src/ward-view/ward-view.test.tsx @@ -9,9 +9,11 @@ import { screen } from '@testing-library/react'; import React from 'react'; import { useParams } from 'react-router-dom'; import { renderWithSwr } from 'tools'; -import { mockWardPatientGroupDetails } from '../../mock'; +import { mockWardPatientGroupDetails, mockWardViewContext } from '../../mock'; import { configSchema } from '../config-schema'; import useWardLocation from '../hooks/useWardLocation'; +import { type WardViewContext } from '../types'; +import DefaultWardView from './default-ward/default-ward-view.component'; import WardView from './ward-view.component'; jest.mocked(useConfig).mockReturnValue({ @@ -37,7 +39,7 @@ jest.mock('react-router-dom', () => ({ })); const mockUseParams = useParams as jest.Mock; -jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails()); +jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); const intersectionObserverMock = () => ({ observe: () => null, @@ -48,33 +50,33 @@ describe('WardView', () => { let replacedProperty: jest.ReplaceProperty | null = null; it('renders the session location when no location provided in URL', () => { - renderWithSwr(); + renderWithSwr(); const header = screen.getByRole('heading', { name: 'mock location' }); expect(header).toBeInTheDocument(); }); it('renders the location provided in URL', () => { mockUseParams.mockReturnValueOnce({ locationUuid: 'abcd' }); - renderWithSwr(); + renderWithSwr(); const header = screen.getByRole('heading', { name: 'mock location' }); expect(header).toBeInTheDocument(); }); it('renders the correct number of occupied and empty beds', async () => { - renderWithSwr(); + renderWithSwr(); const emptyBedCards = await screen.findAllByText(/empty bed/i); expect(emptyBedCards).toHaveLength(3); }); it('renders admitted patient without bed', async () => { - renderWithSwr(); + renderWithSwr(); const admittedPatientWithoutBed = screen.queryByText('Brian Johnson'); expect(admittedPatientWithoutBed).toBeInTheDocument(); }); it('renders all admitted patients even if bed management module not installed', async () => { mockUseFeatureFlag.mockReturnValueOnce(false); - renderWithSwr(); + renderWithSwr(); const admittedPatientWithoutBed = screen.queryByText('Brian Johnson'); expect(admittedPatientWithoutBed).toBeInTheDocument(); }); @@ -100,7 +102,7 @@ describe('WardView', () => { mockUseFeatureFlag.mockReturnValue(true); - renderWithSwr(); + renderWithSwr(); const noBedsConfiguredForThisLocation = screen.queryByText('No beds configured for this location'); expect(noBedsConfiguredForThisLocation).toBeInTheDocument(); }); diff --git a/packages/esm-ward-app/src/ward-view/ward.component.tsx b/packages/esm-ward-app/src/ward-view/ward.component.tsx new file mode 100644 index 000000000..99b572ef4 --- /dev/null +++ b/packages/esm-ward-app/src/ward-view/ward.component.tsx @@ -0,0 +1,106 @@ +import { InlineNotification } from '@carbon/react'; +import { useAppContext, useFeatureFlag } from '@openmrs/esm-framework'; +import classNames from 'classnames'; +import React, { useEffect, useRef, type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import EmptyBedSkeleton from '../beds/empty-bed-skeleton'; +import useWardLocation from '../hooks/useWardLocation'; +import { type WardViewContext } from '../types'; +import styles from './ward-view.scss'; + +const Ward = ({ wardBeds, wardUnassignedPatients }: { wardBeds: ReactNode; wardUnassignedPatients: ReactNode }) => { + const { location } = useWardLocation(); + const { t } = useTranslation(); + const isVertical = useFeatureFlag('ward-view-vertical-tiling'); + + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { bedLayouts } = wardPatientGroupDetails ?? {}; + const { isLoading: isLoadingAdmissionLocation, error: errorLoadingAdmissionLocation } = + wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { + isLoading: isLoadingInpatientAdmissions, + error: errorLoadingInpatientAdmissions, + hasMore: hasMoreInpatientAdmissions, + loadMore: loadMoreInpatientAdmissions, + } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); + + const scrollToLoadMoreTrigger = useRef(null); + useEffect( + function scrollToLoadMore() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (hasMoreInpatientAdmissions && !errorLoadingInpatientAdmissions && !isLoadingInpatientAdmissions) { + loadMoreInpatientAdmissions(); + } + } + }); + }, + { threshold: 1 }, + ); + + if (scrollToLoadMoreTrigger.current) { + observer.observe(scrollToLoadMoreTrigger.current); + } + return () => { + if (scrollToLoadMoreTrigger.current) { + observer.unobserve(scrollToLoadMoreTrigger.current); + } + }; + }, + [scrollToLoadMoreTrigger, hasMoreInpatientAdmissions, errorLoadingInpatientAdmissions, loadMoreInpatientAdmissions], + ); + + if (!wardPatientGroupDetails) return <>; + + return ( +
+ {wardBeds} + {bedLayouts?.length == 0 && isBedManagementModuleInstalled && ( + + )} + {wardUnassignedPatients} + {(isLoadingAdmissionLocation || isLoadingInpatientAdmissions) && } + {errorLoadingAdmissionLocation && ( + + )} + {errorLoadingInpatientAdmissions && ( + + )} +
+
+ ); +}; + +const EmptyBeds = () => { + return ( + <> + {Array(20) + .fill(0) + .map((_, i) => ( + + ))} + + ); +}; + +export default Ward; \ No newline at end of file diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx index 721843920..fd7bfda05 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx @@ -1,16 +1,18 @@ import { Button } from '@carbon/react'; -import { ArrowRightIcon, launchWorkspace, useLayoutType } from '@openmrs/esm-framework'; +import { ArrowRightIcon, launchWorkspace, useAppContext, useLayoutType } from '@openmrs/esm-framework'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import type { WardPatientWorkspaceProps, WardPatientCard } from '../../types'; +import type { WardPatientWorkspaceProps, WardPatientCardType, WardViewContext } from '../../types'; import type { AdmitPatientFormWorkspaceProps } from '../admit-patient-form-workspace/types'; import styles from './admission-request-card.scss'; -const AdmissionRequestCardActions: WardPatientCard = (wardPatient) => { +const AdmissionRequestCardActions: WardPatientCardType = (wardPatient) => { const { patient, inpatientRequest } = wardPatient; const { dispositionType } = inpatientRequest; const { t } = useTranslation(); const responsiveSize = useLayoutType() === 'tablet' ? 'lg' : 'md'; + const {WardPatientHeader} = useAppContext('ward-view-context') ?? {}; + const launchPatientAdmissionForm = useCallback( () => launchWorkspace('admit-patient-form-workspace', { patient, dispositionType }), [], @@ -19,8 +21,9 @@ const AdmissionRequestCardActions: WardPatientCard = (wardPatient) => { const launchPatientTransferForm = useCallback(() => { launchWorkspace('patient-transfer-request-workspace', { wardPatient, + WardPatientHeader }); - }, [wardPatient]); + }, [wardPatient, WardPatientHeader]); return (
diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx index 0a239a1ba..169f85035 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx @@ -1,32 +1,25 @@ -import { ExtensionSlot, formatDatetime, getLocale } from '@openmrs/esm-framework'; +import { formatDatetime, getLocale } from '@openmrs/esm-framework'; import classNames from 'classnames'; import React from 'react'; -import { useCurrentWardCardConfig } from '../../hooks/useCurrentWardCardConfig'; +import { type WardPatientCardType } from '../../types'; +import WardPatientAge from '../../ward-patient-card/row-elements/ward-patient-age'; +import WardPatientGender from '../../ward-patient-card/row-elements/ward-patient-gender.component'; +import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-patient-identifier'; import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name'; -import { WardPatientCardElement } from '../../ward-patient-card/ward-patient-card-element.component'; -import type WardPatientCard from '../../ward-patient-card/ward-patient-card.component'; import styles from './admission-request-card.scss'; -const AdmissionRequestCardHeader: WardPatientCard = (wardPatient) => { +const AdmissionRequestCardHeader: WardPatientCardType = (wardPatient) => { const { inpatientRequest } = wardPatient; const { dispositionEncounter } = inpatientRequest; - const { id, headerRowElements } = useCurrentWardCardConfig(); const { patient } = wardPatient; - const extensionSlotState = wardPatient; - - const rowsExtensionSlotName = id == 'default' ? 'ward-patient-card-slot' : `ward-patient-card-${id}-slot`; return (
- {headerRowElements.map((elementId, i) => ( - - ))} + + +
@@ -38,11 +31,6 @@ const AdmissionRequestCardHeader: WardPatientCard = (wardPatient) => {
{dispositionEncounter?.encounterProviders?.map((provider) => provider?.provider?.display).join(',')}
{dispositionEncounter?.location?.display}
-
); }; diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.component.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.component.tsx index 3c16e6d93..81ab83edb 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.component.tsx @@ -1,13 +1,19 @@ -import React from 'react'; -import type { WardPatientCard } from '../../types'; +import React, { type ReactNode } from 'react'; +import type { WardPatient, WardPatientCardType } from '../../types'; import AdmissionRequestCardActions from './admission-request-card-actions.component'; import AdmissionRequestCardHeader from './admission-request-card-header.component'; import styles from './admission-request-card.scss'; -const AdmissionRequestCard: WardPatientCard = (wardPatient) => { +interface AdmissionRequestCardProps { + wardPatient: WardPatient; + children?: ReactNode; +} + +const AdmissionRequestCard: React.FC = ({ wardPatient, children }) => { return (
+ {children}
); diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss index b7496b63c..08620f49c 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-card/admission-request-card.scss @@ -8,6 +8,11 @@ height: fit-content; color: $color-gray-70; margin-bottom: layout.$spacing-05; + + > div { + margin: layout.$spacing-04; + width: unset; + } } .admissionRequestCardHeaderContainer { @@ -29,7 +34,7 @@ display: none; &:has(div:not(:empty)) { - display: block; + display: block; margin: layout.$spacing-03 0; background-color: white; } diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx index d5e406d69..59c89e035 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx @@ -1,53 +1,26 @@ -import React from 'react'; +import { useAppContext } from '@openmrs/esm-framework'; import { screen } from '@testing-library/react'; -import { defineConfigSchema } from '@openmrs/esm-framework'; -import { useInpatientRequest } from '../../hooks/useInpatientRequest'; -import { configSchema } from '../../config-schema'; -import useWardLocation from '../../hooks/useWardLocation'; -import AdmissionRequestsWorkspace from './admission-requests.workspace'; -import { mockInpatientRequest, mockLocationInpatientWard } from '../../../../../__mocks__'; +import React from 'react'; import { renderWithSwr } from '../../../../../tools'; +import { mockWardViewContext } from '../../../mock'; +import { type WardViewContext } from '../../types'; +import DefaultWardPendingPatients from '../../ward-view/default-ward/default-ward-pending-patients.component'; +import AdmissionRequestsWorkspace, { type AdmissionRequestsWorkspaceProps } from './admission-requests.workspace'; -defineConfigSchema('@openmrs/esm-ward-app', configSchema); - -jest.mock('../../hooks/useInpatientRequest', () => ({ - useInpatientRequest: jest.fn(), -})); -jest.mock('../../hooks/useWardLocation', () => jest.fn()); - -const mockUseWardLocation = useWardLocation as jest.Mock; -mockUseWardLocation.mockReturnValue({ - location: mockLocationInpatientWard, - isLoadingLocation: false, - errorFetchingLocation: null, - invalidLocation: false, -}); - -const mockInpatientRequestResponse: ReturnType = { - error: undefined, - mutate: jest.fn(), - isValidating: false, - isLoading: false, - inpatientRequests: mockInpatientRequest, - totalCount: 1, - hasMore: false, - loadMore: jest.fn(), -}; - -jest.mocked(useInpatientRequest).mockReturnValue(mockInpatientRequestResponse); +jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); -const workspaceProps = { +const workspaceProps: AdmissionRequestsWorkspaceProps = { closeWorkspace: jest.fn(), promptBeforeClosing: jest.fn(), closeWorkspaceWithSavedChanges: jest.fn(), setTitle: jest.fn(), + wardPendingPatients: , }; describe('Admission Requests Workspace', () => { it('should render a admission request card', () => { renderWithSwr(); - expect( - screen.getByText(mockInpatientRequest[0].patient.person?.preferredName?.display as string), - ).toBeInTheDocument(); + const alice = mockWardViewContext.wardPatientGroupDetails.inpatientRequestResponse.inpatientRequests[0].patient; + expect(screen.getByText(alice.person?.preferredName?.display as string)).toBeInTheDocument(); }); }); diff --git a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx index 86a91746a..1be005eb3 100644 --- a/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx @@ -1,21 +1,13 @@ -import React from 'react'; -import styles from './admission-requests-workspace.scss'; -import AdmissionRequestCard from '../admission-request-card/admission-request-card.component'; import { Search } from '@carbon/react'; -import { ErrorState } from '@openmrs/esm-framework'; +import { type DefaultWorkspaceProps } from '@openmrs/esm-framework'; +import React, { type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { useInpatientRequest } from '../../hooks/useInpatientRequest'; -import { type InpatientRequest } from '../../types'; +import styles from './admission-requests-workspace.scss'; -interface AdmissionRequestsWorkspaceProps {} -const AdmissionRequestsWorkspace: React.FC = () => { - // note: useAppContext() does not work here for some reason, so we call `useInpatientRequest` - // directly. See: https://openmrs.atlassian.net/browse/O3-4020 - const { - inpatientRequests, - isLoading: isLoadingInpatientRequests, - error: errorFetchingInpatientRequests, - } = useInpatientRequest(['ADMIT', 'TRANSFER']); +export interface AdmissionRequestsWorkspaceProps extends DefaultWorkspaceProps { + wardPendingPatients: ReactNode; +} +const AdmissionRequestsWorkspace: React.FC = ({ wardPendingPatients }) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = React.useState(''); const handleSearch = (event: React.ChangeEvent) => { @@ -33,29 +25,7 @@ const AdmissionRequestsWorkspace: React.FC = () disabled /> -
- {isLoadingInpatientRequests ? ( - <>Loading - ) : errorFetchingInpatientRequests ? ( - - ) : ( - <> - {inpatientRequests.map((request: InpatientRequest, i) => ( - - ))} - - )} -
+
{wardPendingPatients}
); }; diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx index 210b8b5e2..36477b098 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx @@ -4,12 +4,10 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { mockAdmissionLocation, mockLocationInpatientWard, mockPatientAlice } from '../../../../../__mocks__'; import { renderWithSwr } from '../../../../../tools'; -import { mockWardPatientGroupDetails } from '../../../mock'; -import { useAdmissionLocation } from '../../hooks/useAdmissionLocation'; +import { mockWardPatientGroupDetails, mockWardViewContext } from '../../../mock'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; -import { useInpatientRequest } from '../../hooks/useInpatientRequest'; import useWardLocation from '../../hooks/useWardLocation'; -import type { DispositionType } from '../../types'; +import type { DispositionType, WardViewContext } from '../../types'; import AdmitPatientFormWorkspace from './admit-patient-form.workspace'; import type { AdmitPatientFormWorkspaceProps } from './types'; @@ -40,7 +38,7 @@ const mockedUseFeatureFlag = jest.mocked(useFeatureFlag); const mockedShowSnackbar = jest.mocked(showSnackbar); const mockedUseSession = jest.mocked(useSession); -jest.mocked(useAppContext).mockReturnValue(mockWardPatientGroupDetails()); +jest.mocked(useAppContext).mockReturnValue(mockWardViewContext); const mockWorkspaceProps: AdmitPatientFormWorkspaceProps = { patient: mockPatientAlice, @@ -49,9 +47,6 @@ const mockWorkspaceProps: AdmitPatientFormWorkspaceProps = { promptBeforeClosing: jest.fn(), setTitle: jest.fn(), dispositionType: 'ADMIT', - setCancelTitle: jest.fn(), - setCancelMessage: jest.fn(), - setCancelConfirmText: jest.fn(), }; function renderAdmissionForm(dispositionType: DispositionType = 'ADMIT') { diff --git a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx index 63c0139db..35158d38c 100644 --- a/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; -import type { BedLayout, WardPatientGroupDetails } from '../../types'; +import type { BedLayout, WardViewContext } from '../../types'; import { assignPatientToBed, createEncounter, removePatientFromBed } from '../../ward.resource'; import styles from './admit-patient-form.scss'; import type { AdmitPatientFormWorkspaceProps } from './types'; @@ -25,11 +25,11 @@ const AdmitPatientFormWorkspace: React.FC = ({ const [isSubmitting, setIsSubmitting] = useState(false); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); const [showErrorNotifications, setShowErrorNotifications] = useState(false); - const wardPatientGrouping = useAppContext('ward-patients-group'); - const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGrouping?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardPatientGrouping?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardPatientGrouping?.inpatientAdmissionResponse ?? {}; - const beds = isLoading ? [] : wardPatientGrouping?.bedLayouts ?? []; + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; + const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const beds = isLoading ? [] : wardPatientGroupDetails?.bedLayouts ?? []; const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module'); const getBedRepresentation = useCallback((bedLayout: BedLayout) => { const bedNumber = bedLayout.bedNumber; @@ -92,7 +92,7 @@ const AdmitPatientFormWorkspace: React.FC = ({ if (bedSelected) { return assignPatientToBed(values.bedId, patient.uuid, response.data.uuid); } else { - const bed = wardPatientGrouping.bedLayouts.find((bedLayout) => + const bed = wardPatientGroupDetails.bedLayouts.find((bedLayout) => bedLayout.patients.some((p) => p.uuid == patient.uuid), ); if (bed) { @@ -180,7 +180,7 @@ const AdmitPatientFormWorkspace: React.FC = ({ setIsSubmitting(false); }, []); - if (!wardPatientGrouping) return <>; + if (!wardPatientGroupDetails) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-banner/patient-banner.component.tsx b/packages/esm-ward-app/src/ward-workspace/patient-banner/patient-banner.component.tsx index 3416a2fd0..f543896db 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-banner/patient-banner.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-banner/patient-banner.component.tsx @@ -1,14 +1,11 @@ +import { useAppContext } from '@openmrs/esm-framework'; import React from 'react'; -import type { WardPatient } from '../../types'; -import { WardPatientCardElement } from '../../ward-patient-card/ward-patient-card-element.component'; -import { useCurrentWardCardConfig } from '../../hooks/useCurrentWardCardConfig'; +import type { WardPatient, WardViewContext } from '../../types'; import styles from './style.scss'; -import WardPatientBedNumber from '../../ward-patient-card/row-elements/ward-patient-bed-number'; -import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name'; const WardPatientWorkspaceBanner = (wardPatient: WardPatient) => { - const { headerRowElements } = useCurrentWardCardConfig(); - const { patient, bed } = wardPatient; + const { patient } = wardPatient; + const {WardPatientHeader} = useAppContext('ward-view-context') ?? {}; if (!patient) { console.warn('Patient details were not received by the ward workspace'); @@ -17,11 +14,7 @@ const WardPatientWorkspaceBanner = (wardPatient: WardPatient) => { return (
- {bed ? : null} - - {headerRowElements.map((elementId, i) => ( - - ))} + {WardPatientHeader && }
); }; diff --git a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx index dc74293b7..bf5e00894 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; -import { type WardPatientGroupDetails, type WardPatientWorkspaceProps } from '../../types'; +import { type WardViewContext, type WardPatientWorkspaceProps } from '../../types'; import { createEncounter, removePatientFromBed } from '../../ward.resource'; import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component'; import styles from './patient-discharge.scss'; @@ -17,10 +17,10 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro const { currentProvider } = useSession(); const { location } = useWardLocation(); const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration(); - const wardGroupingDetails = useAppContext('ward-patients-group'); - const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardGroupingDetails?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardGroupingDetails?.inpatientAdmissionResponse ?? {}; + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; + const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; const submitDischarge = useCallback(() => { setIsSubmitting(true); @@ -78,7 +78,7 @@ export default function PatientDischargeWorkspace(props: WardPatientWorkspacePro mutateInpatientAdmission, ]); - if (!wardGroupingDetails) return <>; + if (!wardPatientGroupDetails) return <>; return (
diff --git a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx index 2776c34ee..13f1c4afc 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import useEmrConfiguration from '../../hooks/useEmrConfiguration'; import useWardLocation from '../../hooks/useWardLocation'; -import type { BedLayout, WardPatientGroupDetails, WardPatientWorkspaceProps } from '../../types'; +import type { BedLayout, WardViewContext, WardPatientWorkspaceProps } from '../../types'; import { assignPatientToBed, createEncounter } from '../../ward.resource'; import styles from './patient-transfer-swap.scss'; @@ -32,10 +32,10 @@ export default function PatientBedSwapForm({ const [isSubmitting, setIsSubmitting] = useState(false); const { currentProvider } = useSession(); const { location } = useWardLocation(); - const wardGroupingDetails = useAppContext('ward-patients-group'); - const { isLoading, mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardGroupingDetails?.inpatientRequestResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardGroupingDetails?.inpatientAdmissionResponse ?? {}; + const {wardPatientGroupDetails} = useAppContext('ward-view-context') ?? {}; + const { isLoading, mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; + const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; const zodSchema = useMemo( () => @@ -70,7 +70,7 @@ export default function PatientBedSwapForm({ [t], ); - const beds = wardGroupingDetails?.bedLayouts ?? []; + const beds = wardPatientGroupDetails?.bedLayouts ?? []; const bedDetails = useMemo( () => beds.map((bed) => { @@ -145,7 +145,7 @@ export default function PatientBedSwapForm({ setShowErrorNotifications(true); }, []); - if (!wardGroupingDetails) return <>; + if (!wardPatientGroupDetails) return <>; return ( emrConfiguration?.dispositions.filter(({ type }) => type === 'TRANSFER'), [emrConfiguration], ); - const wardGroupingDetails = useAppContext('ward-patients-group'); - const { mutate: mutateAdmissionLocation } = wardGroupingDetails?.admissionLocationResponse ?? {}; - const { mutate: mutateInpatientAdmission } = wardGroupingDetails?.inpatientAdmissionResponse ?? {}; - const { mutate: mutateInpatientRequest } = wardGroupingDetails?.inpatientRequestResponse ?? {}; + const { wardPatientGroupDetails } = useAppContext('ward-view-context') ?? {}; + const { mutate: mutateAdmissionLocation } = wardPatientGroupDetails?.admissionLocationResponse ?? {}; + const { mutate: mutateInpatientAdmission } = wardPatientGroupDetails?.inpatientAdmissionResponse ?? {}; + const { mutate: mutateInpatientRequest } = wardPatientGroupDetails?.inpatientRequestResponse ?? {}; const zodSchema = useMemo( () => @@ -157,7 +157,7 @@ export default function PatientTransferForm({ setShowErrorNotifications(true); }, []); - if (!wardGroupingDetails) return <>; + if (!wardPatientGroupDetails) return <>; return ( Date: Tue, 15 Oct 2024 22:22:54 +0300 Subject: [PATCH 2/3] (refactor) Port modal registrations to use the modal system (#1344) This PR ports over some left over modal registrations to use the modal system instead of the legacy extension modal registry. It also removes an [unused](https://github.com/search?q=org:openmrs+check-in-appointment-modal&type=code) modal registration from the Appointments app routes registry. --- packages/esm-appointments-app/src/routes.json | 26 ++++---- .../esm-service-queues-app/src/routes.json | 61 ++++++++++--------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/packages/esm-appointments-app/src/routes.json b/packages/esm-appointments-app/src/routes.json index f9985d8dc..fbe5d8fac 100644 --- a/packages/esm-appointments-app/src/routes.json +++ b/packages/esm-appointments-app/src/routes.json @@ -3,12 +3,6 @@ "backendDependencies": { "webservices.rest": "^2.2.0" }, - "modals": [ - { - "name": "end-appointment-modal", - "component": "endAppointmentModal" - } - ], "extensions": [ { "name": "home-appointments", @@ -36,11 +30,7 @@ "slot": "calendar-dashboard-slot", "component": "appointmentsCalendarDashboardLink" }, - { - "name": "check-in-appointment-modal", - "slot": "todays-appointment-slot", - "component": "checkInModal" - }, + { "name": "todays-appointments-dashboard", "slot": "todays-appointment-slot", @@ -111,10 +101,6 @@ "component": "patientUpcomingAppointmentsWidget", "slot": "upcoming-appointment-slot" }, - { - "name": "patient-appointment-cancel-confirmation-dialog", - "component": "patientAppointmentsCancelConfirmationDialog" - }, { "name": "edit-appointments-form", "component": "appointmentsForm", @@ -154,5 +140,15 @@ "slot": "home-metrics-tiles-slot", "component": "homeAppointmentsTile" } + ], + "modals": [ + { + "name": "end-appointment-modal", + "component": "endAppointmentModal" + }, + { + "name": "patient-appointment-cancel-confirmation-dialog", + "component": "patientAppointmentsCancelConfirmationDialog" + } ] } diff --git a/packages/esm-service-queues-app/src/routes.json b/packages/esm-service-queues-app/src/routes.json index d0e298751..4d92a7168 100644 --- a/packages/esm-service-queues-app/src/routes.json +++ b/packages/esm-service-queues-app/src/routes.json @@ -40,10 +40,6 @@ "name": "service-queues-dashboard", "slot": "service-queues-dashboard-slot" }, - { - "name": "edit-queue-entry-status-modal", - "component": "editQueueEntryStatusModal" - }, { "name": "patient-info-banner-slot", "component": "patientInfoBannerSlot" @@ -57,55 +53,62 @@ "component": "clearAllQueueEntries" }, { - "name": "add-visit-to-queue-modal", - "component": "addVisitToQueueModal" + "name": "previous-visit-summary-widget", + "component": "pastVisitSummary", + "slot": "previous-visit-summary-slot" }, + { - "name": "transition-queue-entry-status-modal", - "component": "transitionQueueEntryStatusModal" + "name": "active-visits-row-actions", + "component": "activeVisitsRowActions", + "slot": "queue-table-serve-patient-slot" }, { - "name": "previous-visit-summary-widget", - "component": "pastVisitSummary", - "slot": "previous-visit-summary-slot" + "name": "visit-form-queue-fields", + "component": "visitFormQueueFields", + "slot":"visit-form-queue-slot" + } + ], + "modals": [ + { + "name": "add-visit-to-queue-modal", + "component": "addVisitToQueueModal" }, { "name": "add-provider-to-room-modal", "component": "addProviderToRoomModal" }, { - "name": "transition-queue-entry-modal", - "component": "transitionQueueEntryModal" + "name": "edit-queue-entry-modal", + "component": "editQueueEntryModal" }, { - "name": "transition-patient-to-latest-queue-modal", - "component": "transitionPatientToLatestQueue" + "name": "edit-queue-entry-status-modal", + "component": "editQueueEntryStatusModal" }, { - "name": "edit-queue-entry-modal", - "component": "editQueueEntryModal" + "name": "end-queue-entry-modal", + "component": "endQueueEntryModal" }, { - "name": "undo-transition-queue-entry-modal", - "component": "undoTransitionQueueEntryModal" + "name": "transition-patient-to-latest-queue-modal", + "component": "transitionPatientToLatestQueue" }, { - "name": "void-queue-entry-modal", - "component": "voidQueueEntryModal" + "name": "transition-queue-entry-modal", + "component": "transitionQueueEntryModal" }, { - "name": "end-queue-entry-modal", - "component": "endQueueEntryModal" + "name": "transition-queue-entry-status-modal", + "component": "transitionQueueEntryStatusModal" }, { - "name": "active-visits-row-actions", - "component": "activeVisitsRowActions", - "slot": "queue-table-serve-patient-slot" + "name": "undo-transition-queue-entry-modal", + "component": "undoTransitionQueueEntryModal" }, { - "name": "visit-form-queue-fields", - "component": "visitFormQueueFields", - "slot":"visit-form-queue-slot" + "name": "void-queue-entry-modal", + "component": "voidQueueEntryModal" } ], "workspaces": [ From d5c2f3ecc82ade34ee5ea9e3dcf9c40249876ac5 Mon Sep 17 00:00:00 2001 From: Bhargav kodali <115476530+kb019@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:29:55 -0700 Subject: [PATCH 3/3] (fix) O3-4004 - ward app - better way to fix patient action menu button in workspace (#1346) * add depedency in use effect * undo changes in o3-4004 * correct type --- packages/esm-ward-app/src/types/index.ts | 2 ++ .../ward-patient-card.component.tsx | 12 +++++------- .../ward-patient-resource.ts | 16 ---------------- .../ward-patient-action-button.extension.tsx | 4 ++-- .../ward-patient.workspace.tsx | 19 +++++++++++++------ 5 files changed, 22 insertions(+), 31 deletions(-) delete mode 100644 packages/esm-ward-app/src/ward-patient-card/ward-patient-resource.ts diff --git a/packages/esm-ward-app/src/types/index.ts b/packages/esm-ward-app/src/types/index.ts index 92fc41166..0492839f9 100644 --- a/packages/esm-ward-app/src/types/index.ts +++ b/packages/esm-ward-app/src/types/index.ts @@ -231,3 +231,5 @@ export interface WardViewContext { export interface MaternalWardViewContext { motherChildrenRelationshipsByPatient: Map; } + +export type PatientWorkspaceAdditionalProps = Omit; \ No newline at end of file diff --git a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx b/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx index 0916a5f4a..a6ed06ec0 100644 --- a/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx +++ b/packages/esm-ward-app/src/ward-patient-card/ward-patient-card.component.tsx @@ -1,8 +1,7 @@ -import { getPatientName, useAppContext } from '@openmrs/esm-framework'; +import { getPatientName, launchWorkspace, useAppContext } from '@openmrs/esm-framework'; import React, { type ReactNode } from 'react'; -import { type WardViewContext, type WardPatient } from '../types'; +import { type WardViewContext, type WardPatient,type PatientWorkspaceAdditionalProps } from '../types'; import styles from './ward-patient-card.scss'; -import { launchPatientWorkspace, setPatientWorkspaceProps } from './ward-patient-resource'; interface Props { children: ReactNode; @@ -19,12 +18,11 @@ const WardPatientCard: React.FC = ({ children, wardPatient }) => { diff --git a/packages/esm-ward-app/src/ward-patient-card/ward-patient-resource.ts b/packages/esm-ward-app/src/ward-patient-card/ward-patient-resource.ts deleted file mode 100644 index f1284471a..000000000 --- a/packages/esm-ward-app/src/ward-patient-card/ward-patient-resource.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type DefaultWorkspaceProps, launchWorkspace } from '@openmrs/esm-framework'; -import { type WardPatientWorkspaceProps } from '../types'; - -type PatientWorkspaceAdditionalProps = Omit; - -// workspaces launched from workspace action menu buttons sometimes lose -// access to their props. This serves as a workaround. -// See: https://openmrs.atlassian.net/browse/O3-4004 -let props: PatientWorkspaceAdditionalProps = null; -export function setPatientWorkspaceProps(newProps: PatientWorkspaceAdditionalProps) { - props = newProps; -} - -export function launchPatientWorkspace() { - launchWorkspace('ward-patient-workspace', props); -} diff --git a/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx b/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx index be36c5403..7be3a9a86 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { UserAvatarIcon } from '@openmrs/esm-framework'; import { ActionMenuButton, launchWorkspace } from '@openmrs/esm-framework'; -import { launchPatientWorkspace } from '../../ward-patient-card/ward-patient-resource'; +import type { WardPatientWorkspaceProps } from '../../types'; export default function WardPatientActionButton() { const { t } = useTranslation(); @@ -12,7 +12,7 @@ export default function WardPatientActionButton() { getIcon={(props) => } label={t('Patient', 'patient')} iconDescription={t('Patient', 'patient')} - handler={() => launchPatientWorkspace()} + handler={() => launchWorkspace('ward-patient-workspace')} type={'ward'} /> ); diff --git a/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient.workspace.tsx b/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient.workspace.tsx index cfd9e51ee..f96cf342a 100644 --- a/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient.workspace.tsx +++ b/packages/esm-ward-app/src/ward-workspace/patient-details/ward-patient.workspace.tsx @@ -7,15 +7,22 @@ import { getGender } from '../../ward-patient-card/row-elements/ward-patient-gen attach('ward-patient-workspace-header-slot', 'patient-vitals-info'); -export default function WardPatientWorkspace({ setTitle, wardPatient: { patient } }: WardPatientWorkspaceProps) { +export default function WardPatientWorkspace({ setTitle, wardPatient }: WardPatientWorkspaceProps) { useEffect(() => { - setTitle(patient.person.display, ); - }, [patient.uuid]); + if (wardPatient) { + const { patient } = wardPatient; + setTitle(patient.person.display, ); + } + }, [wardPatient]); return ( -
- -
+ <> + {wardPatient && ( +
+ +
+ )} + ); }