Skip to content

Commit

Permalink
(feat) Patient lists UI fixes and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
denniskigen committed Oct 28, 2023
1 parent b0604ed commit ae8b600
Show file tree
Hide file tree
Showing 26 changed files with 566 additions and 415 deletions.
1 change: 1 addition & 0 deletions packages/esm-patient-list-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@carbon/react": "^1.12.0",
"dexie": "^3.0.3",
"fuzzy": "^0.1.3",
"lodash-es": "^4.17.15"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,8 @@ const CreateEditPatientList: React.FC<CreateEditPatientListProps> = ({
})
.then(() =>
showToast({
title: t('successCreatedPatientList', 'Created patient list'),
description: `${t('successCreatedPatientListDescription', 'Successfully created patient list')} : ${
cohortDetails?.name
}`,
title: t('created', 'Created'),
description: `${t('listCreated', 'List created successfully')}`,
kind: 'success',
}),
)
Expand All @@ -62,12 +60,10 @@ const CreateEditPatientList: React.FC<CreateEditPatientListProps> = ({
setSubmitting(false);
})
.then(close)
.catch(() => {
.catch((e) => {
showToast({
title: t('error', 'Error'),
description: `${t('errorCreatePatientListDescription', "Couldn't create patient list")} : ${
cohortDetails?.name
}`,
title: t('error', 'Error creating list'),
description: e?.message,
kind: 'error',
});
setSubmitting(false);
Expand All @@ -76,8 +72,8 @@ const CreateEditPatientList: React.FC<CreateEditPatientListProps> = ({
editPatientList(patientListDetails.uuid, cohortDetails)
.then(() =>
showToast({
title: t('successUpdatePatientList', 'Updated patient list'),
description: t('successUpdatePatientListDescription', 'Successfully updated patient list'),
title: t('updated', 'Updated'),
description: t('listUpdated', 'List updated successfully'),
kind: 'success',
}),
)
Expand All @@ -86,12 +82,10 @@ const CreateEditPatientList: React.FC<CreateEditPatientListProps> = ({
setSubmitting(false);
})
.then(close)
.catch(() => {
.catch((e) => {
showToast({
title: t('error', 'Error'),
description: `${t('errorUpdatePatientListDescription', "Couldn't update patient list")} : ${
cohortDetails?.name
}`,
title: t('errorUpdatingList', 'Error updating list'),
description: e?.message,
kind: 'error',
});
setSubmitting(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface EmptyStateProps {
launchForm?(): void;
}

export const PatientListEmptyState: React.FC<EmptyStateProps> = ({ listType, launchForm }) => {
const EmptyState: React.FC<EmptyStateProps> = ({ listType, launchForm }) => {
const { t } = useTranslation();

return (
Expand All @@ -37,3 +37,5 @@ export const PatientListEmptyState: React.FC<EmptyStateProps> = ({ listType, lau
</Layer>
);
};

export default EmptyState;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ErrorStateProps {
headerTitle: string;
}

export const ErrorState: React.FC<ErrorStateProps> = ({ error, headerTitle }) => {
const ErrorState: React.FC<ErrorStateProps> = ({ error, headerTitle }) => {
const { t } = useTranslation('@openmrs/esm-patient-list-app');
const isTablet = useLayoutType() === 'tablet';

Expand All @@ -33,3 +33,5 @@ export const ErrorState: React.FC<ErrorStateProps> = ({ error, headerTitle }) =>
</Layer>
);
};

export default ErrorState;
51 changes: 51 additions & 0 deletions packages/esm-patient-list-app/src/header/header.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@carbon/react';
import { Add, Calendar } from '@carbon/react/icons';
import { formatDate, navigate } from '@openmrs/esm-framework';
import Illustration from '../illo';
import styles from './header.scss';

const Header: React.FC = () => {
const { t } = useTranslation();
const newCohortUrl = window.getOpenmrsSpaBase() + 'home/patient-lists?new_cohort=true';

const handleShowNewListOverlay = () => {
// URL navigation is in place to know either to open the create list overlay or not
// The url /patient-list?new_cohort=true is being used in the "Add patient to list" widget
// in the patient chart. The button in the above mentioned widget "Create new list", navigates
// to /patient-list?new_cohort=true to open the overlay directly.
navigate({
to: newCohortUrl,
});
};

return (
<div className={styles.patientListHeader}>
<div className={styles.leftJustifiedItems}>
<Illustration />
<div className={styles.pageLabels}>
<p>{t('patientLists', 'Patient lists')}</p>
<p className={styles.pageName}>{t('home', 'Home')}</p>
</div>
</div>
<div className={styles.rightJustifiedItems}>
<div className={styles.date}>
<Calendar size={16} />
<span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
</div>
<Button
className={styles.newListButton}
kind="ghost"
iconDescription="Add"
renderIcon={(props) => <Add {...props} size={16} />}
onClick={handleShowNewListOverlay}
size="sm">
{t('newList', 'New list')}
</Button>
</div>
</div>
);
};

export default Header;
52 changes: 52 additions & 0 deletions packages/esm-patient-list-app/src/header/header.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@import '../style.scss';

.patientListHeader {
@include type.type-style('body-compact-02');
color: $text-02;
height: spacing.$spacing-12;
display: flex;
justify-content: space-between;
}

.leftJustifiedItems {
display: flex;
flex-direction: row;
align-items: center;
}

.rightJustifiedItems {
@include type.type-style('body-compact-02');
display: flex;
flex-direction: column;
color: $text-02;
padding: 1rem 0;
justify-content: space-between;
}

.date {
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: 1rem;

svg {
margin-right: 0.5rem;
}
}

.pageName {
@include type.type-style('heading-04');
}

.pageLabels {
p:first-of-type {
margin-bottom: 0.25rem;
}
}

.newListButton {
align-self: flex-end;
width: fit-content;
}
2 changes: 1 addition & 1 deletion packages/esm-patient-list-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const patientListActionButton = getAsyncLifecycle(() => import('./patient

export const patientListDashboardLink = getSyncLifecycle(createDashboardLink(dashboardMeta), options);

export const patientTable = getAsyncLifecycle(() => import('./patient-table/patient-table.component'), {
export const patientTable = getAsyncLifecycle(() => import('./list-details-table/list-details-table.component'), {
featureName: 'patient-table',
moduleName,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { HTMLAttributes, useMemo, useState, useCallback, type CSSProperties } from 'react';
import debounce from 'lodash-es/debounce';
import fuzzy from 'fuzzy';
import { useTranslation } from 'react-i18next';
import {
Button,
Expand All @@ -21,10 +21,10 @@ import {
Tile,
} from '@carbon/react';
import { ArrowLeft, TrashCan } from '@carbon/react/icons';
import { ConfigurableLink, useLayoutType, isDesktop, showToast } from '@openmrs/esm-framework';
import { ConfigurableLink, useLayoutType, isDesktop, showToast, useDebounce } from '@openmrs/esm-framework';
import { removePatientFromList } from '../api/api-remote';
import { EmptyDataIllustration } from '../patient-list-list/empty-state/empty-data-illustration.component';
import styles from './patient-table.scss';
import { EmptyDataIllustration } from '../empty-state/empty-data-illustration.component';
import styles from './list-details-table.scss';

// FIXME Temporarily included types from Carbon
type InputPropsBase = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'>;
Expand Down Expand Up @@ -120,7 +120,7 @@ interface SearchProps extends InputPropsBase {
value?: string | number;
}

interface PatientTableProps {
interface ListDetailsTableProps {
patients;
columns: Array<PatientTableColumn>;
style?: CSSProperties;
Expand All @@ -129,12 +129,6 @@ interface PatientTableProps {
isFetching?: boolean;
mutateListDetails: () => void;
mutateListMembers: () => void;
search: {
onSearch(searchTerm: string): any;
placeHolder: string;
currentSearchTerm?: string;
otherSearchProps?: SearchProps;
};
pagination: {
usePagination: boolean;
currentPage: number;
Expand All @@ -155,10 +149,9 @@ interface PatientTableColumn {
};
}

const PatientTable: React.FC<PatientTableProps> = ({
const ListDetailsTable: React.FC<ListDetailsTableProps> = ({
patients,
columns,
search,
pagination,
isLoading,
isFetching,
Expand All @@ -167,16 +160,34 @@ const PatientTable: React.FC<PatientTableProps> = ({
}) => {
const { t } = useTranslation();
const layout = useLayoutType();
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
const patientListsPath = window.getOpenmrsSpaBase() + 'home/patient-lists';

const [patientName, setPatientName] = useState('');
const [isDeleting, setIsDeleting] = useState(false);
const [membershipUuid, setMembershipUuid] = useState('');
const [patientName, setPatientName] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const debouncedSearchTerm = useDebounce(searchTerm);

const filteredPatients = useMemo(() => {
if (!debouncedSearchTerm) {
return patients;
}

const rows: Array<typeof DataTableRow> = useMemo(
return debouncedSearchTerm
? fuzzy
.filter(debouncedSearchTerm, patients, {
extract: (patient: any) => `${patient.name} ${patient.identifier} ${patient.sex}`,
})
.sort((r1, r2) => r1.score - r2.score)
.map((result) => result.original)
: patients;
}, [debouncedSearchTerm, patients]);

const tableRows: Array<typeof DataTableRow> = useMemo(
() =>
patients.map((patient) => ({
filteredPatients?.map((patient) => ({
id: patient.identifier,
identifier: patient.identifier,
membershipUuid: patient.membershipUuid,
Expand All @@ -191,12 +202,10 @@ const PatientTable: React.FC<PatientTableProps> = ({
),
sex: patient.sex,
startDate: patient.startDate,
})),
[columns, patients],
})) ?? [],
[columns, filteredPatients],
);

const handleSearch = useMemo(() => debounce((searchTerm) => search.onSearch(searchTerm), 300), [search]);

const handleRemovePatientFromList = useCallback(async () => {
setIsDeleting(true);

Expand Down Expand Up @@ -224,8 +233,6 @@ const PatientTable: React.FC<PatientTableProps> = ({
setShowConfirmationModal(false);
}, [membershipUuid, mutateListDetails, mutateListMembers, t]);

const otherSearchProps = useMemo(() => search.otherSearchProps || {}, [search]);

const BackButton = () => (
<div className={styles.backButton}>
<ConfigurableLink to={patientListsPath}>
Expand Down Expand Up @@ -265,19 +272,17 @@ const PatientTable: React.FC<PatientTableProps> = ({
<div>
<Layer>
<Search
className={styles.searchOverrides}
id="patient-list-search"
placeholder={search.placeHolder}
labelText=""
size={isDesktop(layout) ? 'sm' : 'lg'}
className={styles.searchOverrides}
onChange={(event) => handleSearch(event.target.value)}
defaultValue={search.currentSearchTerm}
{...otherSearchProps}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
placeholder={t('searchThisList', 'Search this list')}
size={responsiveSize}
/>
</Layer>
</div>
</div>
<DataTable rows={rows} headers={columns} isSortable size={isDesktop(layout) ? 'sm' : 'lg'} useZebraStyles>
<DataTable rows={tableRows} headers={columns} isSortable size={responsiveSize} useZebraStyles>
{({ rows, headers, getHeaderProps, getTableProps, getRowProps }) => (
<TableContainer>
<Table className={styles.table} {...getTableProps()} data-testid="patientsTable">
Expand Down Expand Up @@ -314,7 +319,7 @@ const PatientTable: React.FC<PatientTableProps> = ({
hasIconOnly
renderIcon={TrashCan}
iconDescription={t('removeFromList', 'Remove from list')}
size={isDesktop(layout) ? 'sm' : 'lg'}
size={responsiveSize}
tooltipPosition="left"
onClick={() => {
setMembershipUuid(row.id);
Expand All @@ -331,6 +336,18 @@ const PatientTable: React.FC<PatientTableProps> = ({
</TableContainer>
)}
</DataTable>
{filteredPatients?.length === 0 && (
<div className={styles.filterEmptyState}>
<Layer level={0}>
<Tile className={styles.filterEmptyStateTile}>
<p className={styles.filterEmptyStateContent}>
{t('noMatchingPatients', 'No matching patients to display')}
</p>
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
</Tile>
</Layer>
</div>
)}
{pagination.usePagination && (
<Pagination
page={pagination.currentPage}
Expand Down Expand Up @@ -383,4 +400,4 @@ const PatientTable: React.FC<PatientTableProps> = ({
);
};

export default PatientTable;
export default ListDetailsTable;
Loading

0 comments on commit ae8b600

Please sign in to comment.