Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-3247: Add forms for transfer patient and swap bed #1267

Merged
merged 13 commits into from
Aug 12, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
ActionMenuButton,
getGlobalStore,
launchWorkspace,
MovementIcon,
type DefaultWorkspaceProps,
} from '@openmrs/esm-framework';
import React from 'react';
import { useTranslation } from 'react-i18next';

interface PatientTransferAndSwapSiderailIconProps extends DefaultWorkspaceProps {}

export default function PatientTransferAndSwapSiderailIcon(additionalProps: PatientTransferAndSwapSiderailIconProps) {
const { t } = useTranslation();
const handler = () => {
launchWorkspace('patient-transfer-swap-workspace');
};
return (
<ActionMenuButton
getIcon={(props) => <MovementIcon {...props} />}
label={t('transfers', 'Transfers')}
iconDescription={t('transfers', 'Transfers')}
handler={handler}
type="ward"
/>
);
}
21 changes: 21 additions & 0 deletions packages/esm-ward-app/src/hooks/useEmrConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import { type FetchResponse, openmrsFetch, type OpenmrsResource, restBaseUrl } from '@openmrs/esm-framework';
import { useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
import type { DispositionType } from '../types';

interface EmrApiConfigurationResponse {
admissionEncounterType: OpenmrsResource;
clinicianEncounterRole: OpenmrsResource;
consultFreeTextCommentsConcept: OpenmrsResource;
visitNoteEncounterType: OpenmrsResource;
transferWithinHospitalEncounterType: OpenmrsResource;
supportsTransferLocationTag: OpenmrsResource;
dispositionDescriptor: {
admissionLocationConcept: OpenmrsResource;
dateOfDeathConcept: OpenmrsResource;
dispositionConcept: OpenmrsResource;
internalTransferLocationConcept: OpenmrsResource;
dispositionSetConcept: OpenmrsResource;
};
dispositions: Array<{
encounterTypes: null;
keepsVisitOpen: null;
additionalObs: null;
careSettingTypes: ['OUTPATIENT'];
name: string;
conceptCode: string;
type: DispositionType;
actions: [];
excludedEncounterTypes: Array<string>;
uuid: string;
}>;
// There are many more keys to this object, but we only need these for now
// Add more keys as needed
}
Expand Down
64 changes: 64 additions & 0 deletions packages/esm-ward-app/src/hooks/useLocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type FetchResponse, fhirBaseUrl, openmrsFetch } from '@openmrs/esm-framework';
import { useEffect, useMemo, useState } from 'react';
import useSWRImmutable from 'swr/immutable';

interface FhirLocation {
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
fullUrl: string;
resource: {
resourceType: 'Location';
id: string;
name: string;
description: string;
};
}

interface FhirResponse {
resourceType: 'Bundle';
id: '6a107c31-d760-4df0-bb70-89ad742225ca';
meta: {
lastUpdated: '2024-08-08T06:28:01.495+00:00';
};
type: 'searchset';
total: number;
link: Array<{
relation: 'self' | 'prev' | 'next';
url: string;
}>;
entry: Array<FhirLocation>;
}

function getUrl(url: string) {
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
const urlObj = new URL(url);
// For production
if (urlObj.origin === window.location.origin) {
return url;
}

return new URL(`${urlObj.pathname}${urlObj.search ? `?${urlObj.search}` : ''}`, window.location.origin).href;
}

export default function useLocations(filterCriteria: Array<Array<string>> = [], skip: boolean = false) {
const [totalLocations, setTotalLocations] = useState(0);
const [url, setUrl] = useState(`${fhirBaseUrl}/Location`);
const searchParams = new URLSearchParams(filterCriteria);
const urlWithSearchParams = `${url}?${searchParams.toString()}`;
const { data, ...rest } = useSWRImmutable<FetchResponse<FhirResponse>>(
!skip ? urlWithSearchParams : null,
openmrsFetch,
);

useEffect(() => {
if (data?.data) {
setTotalLocations(data.data.total);
}
}, [data]);

const results = useMemo(() => {
return {
locations: data?.data?.entry.map((entry) => entry.resource),
totalLocations,
...rest,
};
}, [data, rest, totalLocations]);
return results;
}
11 changes: 11 additions & 0 deletions packages/esm-ward-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ export const wardPatientNotesActionButtonExtension = getAsyncLifecycle(

export const coloredObsTagCardRowExtension = getSyncLifecycle(ColoredObsTagsCardRowExtension, options);

// t('transfers', 'Transfers')
export const patientTransferAndSwapWorkspace = getAsyncLifecycle(
() => import('./ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace'),
options,
);

export const patientTransferAndSwapWorkspaceSiderailIcon = getAsyncLifecycle(
() => import('./action-menu-buttons/transfer-workspace-siderail.component'),
options,
);

export function startupApp() {
registerBreadcrumbs([]);
defineConfigSchema(moduleName, configSchema);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useCallback, useMemo, useState } from 'react';
import styles from './location-selector.scss';
import useLocations from '../hooks/useLocations';
import { RadioButton, Search, RadioButtonGroup, RadioButtonSkeleton } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { Button } from '@carbon/react';
import {
ChevronLeftIcon,
ChevronRightIcon,
isDesktop,
ResponsiveWrapper,
useDebounce,
useLayoutType,
} from '@openmrs/esm-framework';
import classNames from 'classnames';
import type { RadioButtonGroupProps } from '@carbon/react/lib/components/RadioButtonGroup/RadioButtonGroup';
import useEmrConfiguration from '../hooks/useEmrConfiguration';

const size = 5;

interface LocationSelectorProps extends RadioButtonGroupProps {}

export default function LocationSelector(props: LocationSelectorProps) {
const { t } = useTranslation();
const { emrConfiguration, isLoadingEmrConfiguration } = useEmrConfiguration();
const isTablet = !isDesktop(useLayoutType());
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm);
const [page, setPage] = useState(1);
const filterCriteria: Array<Array<string>> = useMemo(() => {
const criteria = [];
if (debouncedSearchTerm) {
criteria.push(['name:contains', debouncedSearchTerm]);
}
criteria.push(['_count', size.toString()]);
if (emrConfiguration) {
criteria.push(['_tag', emrConfiguration.supportsTransferLocationTag.display]);
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
}
if (page > 1) {
criteria.push(['_getpagesoffset', ((page - 1) * size).toString()]);
}
return criteria;
}, [debouncedSearchTerm, page]);
const { locations, isLoading, totalLocations } = useLocations(filterCriteria, !emrConfiguration);

const handlePageChange = useCallback(
({ page: newPage }) => {
setPage(newPage);
},
[setPage, page],
);
const handleSearch = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
},
[setSearchTerm],
);
return (
<div className={styles.locationSelector}>
<ResponsiveWrapper>
<Search
onChange={handleSearch}
value={searchTerm}
placeholder={t('searchLocations', 'Search locations')}
size={isTablet ? 'lg' : 'md'}
/>
</ResponsiveWrapper>
{isLoading || isLoadingEmrConfiguration ? (
<div className={styles.radioButtonGroup}>
<RadioButtonSkeleton />
<RadioButtonSkeleton />
<RadioButtonSkeleton />
<RadioButtonSkeleton />
<RadioButtonSkeleton />
</div>
) : (
<ResponsiveWrapper>
<RadioButtonGroup {...props} className={styles.radioButtonGroup} value="" orientation="vertical">
{locations?.map((location) => (
<RadioButton key={location.id} labelText={location.name} value={location.id} />
))}
</RadioButtonGroup>
</ResponsiveWrapper>
)}
{totalLocations > 5 && (
<div className={styles.pagination}>
<span className={styles.bodyShort01}>
{t('showingLocations', '{{start}}-{{end}} of {{count}} locations', {
start: (page - 1) * size + 1,
end: Math.min(page * size, totalLocations),
count: totalLocations,
})}
</span>
<div>
<Button
className={classNames(styles.button, styles.buttonLeft)}
disabled={page === 1}
onClick={() => handlePageChange({ page: page - 1 })}
hasIconOnly
renderIcon={ChevronLeftIcon}
kind="ghost"
iconDescription={t('previousPage', 'Previous page')}
/>
<Button
className={styles.button}
disabled={page * size >= totalLocations}
onClick={() => handlePageChange({ page: page + 1 })}
hasIconOnly
renderIcon={ChevronRightIcon}
kind="ghost"
iconDescription={t('nextPage', 'Next page')}
/>
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
)}
</div>
);
}
51 changes: 51 additions & 0 deletions packages/esm-ward-app/src/location-selector/location-selector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
@use '@carbon/layout';
@import '~@openmrs/esm-styleguide/src/vars';
@import '../root.scss';

.locationSelector {
border: 1px solid $ui-03;
background-color: $ui-01;
}

:global(.omrs-breakpoint-lt-desktop) .locationSelector {
background-color: $ui-02;
}

.radioButtonGroup {
padding: layout.$spacing-03 layout.$spacing-05;
width: 100%;

:global(.cds--radio-button-wrapper) {
padding: layout.$spacing-02;

:global(.cds--radio-button__label) {
align-items: center;
}
}
}

.pagination {
display: flex;
padding-left: layout.$spacing-05;
justify-content: space-between;
align-items: center;
border-top: 1px solid $ui-03;
}

.button {
svg {
fill: $ui-05 !important;
}
}

.buttonLeft {
border-right: 1px solid $ui-03 !important;
border-left: 1px solid $ui-03 !important;
}

.borderedButton {
border-left: 1px solid $ui-03;
border-right: 1px solid $ui-03;
}
4 changes: 4 additions & 0 deletions packages/esm-ward-app/src/root.scss
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
@include type.type-style('body-01');
}

.bodyShort02 {
@include type.type-style('body-compact-01');
}

.bodyShort02 {
@include type.type-style('body-compact-02');
}
Expand Down
13 changes: 13 additions & 0 deletions packages/esm-ward-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
"component": "coloredObsTagCardRowExtension",
"name": "colored-obs-tags-card-row",
"slot": "ward-patient-card-slot"
},
{
"name": "transfer-swap-patient-siderail-button",
"slot": "action-menu-ward-patient-items-slot",
"component": "patientTransferAndSwapWorkspaceSiderailIcon"
}
],
"workspaces": [
Expand Down Expand Up @@ -75,6 +80,14 @@
"width": "extra-wide",
"hasOwnSidebar": true,
"sidebarFamily": "ward-patient"
},
{
"name": "patient-transfer-swap-workspace",
"component": "patientTransferAndSwapWorkspace",
"title": "transfers",
"type": "transfer-swap-bed-form",
"hasOwnSidebar": true,
"sidebarFamily": "ward-patient"
}
]
}
7 changes: 2 additions & 5 deletions packages/esm-ward-app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,7 @@ export interface EncounterPayload {
}

export interface ObsPayload {
concept: Concept;
concept: Concept | string;
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
value?: string;
groupMembers?: Array<{
concept: Concept;
value: string;
}>;
groupMembers?: Array<ObsPayload>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const WardPatientCard: React.FC<WardPatientCardProps> = ({
<button
className={styles.wardPatientCardButton}
onClick={() => {
launchWorkspace<WardPatientWorkspaceProps>('ward-patient-workspace', {
launchWorkspace<WardPatientWorkspaceProps>('patient-transfer-swap-workspace', {
patientUuid: patient.uuid,
patient,
visit,
Expand Down
Loading
Loading