Skip to content

Commit

Permalink
(feat) Support offline in forms dashboard (#1437)
Browse files Browse the repository at this point in the history
  • Loading branch information
icrc-jofrancisco authored Nov 9, 2023
1 parent 1b819bc commit 881258a
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
usePatient,
} from '@openmrs/esm-framework';
import { useParams } from 'react-router-dom';
import { changeWorkspaceContext, useAutoCreatedOfflineVisit, useWorkspaces } from '@openmrs/esm-patient-common-lib';
import { changeWorkspaceContext, useWorkspaces } from '@openmrs/esm-patient-common-lib';
import { spaBasePath } from '../constants';
import { LayoutMode } from './chart-review/dashboard-view.component';
import ActionMenu from './action-menu/action-menu.component';
Expand All @@ -24,14 +24,12 @@ const PatientChart: React.FC = () => {
const { isLoading: isLoadingPatient, patient } = usePatient(patientUuid);
const { workspaceWindowState, active } = useWorkspaces();
const state = useMemo(() => ({ patient, patientUuid }), [patient, patientUuid]);
const { offlineVisitTypeUuid } = useConfig();
const [layoutMode, setLayoutMode] = useState<LayoutMode>();

// We are responsible for creating a new offline visit while in offline mode.
// The patient chart widgets assume that this is handled by the chart itself.
// We are also the module that holds the offline visit type UUID config.
// The following hook takes care of the creation.
useAutoCreatedOfflineVisit(patientUuid, offlineVisitTypeUuid);

// Keep state updated with the current patient. Anything used outside the patient
// chart (e.g., the current visit is used by the Active Visit Tag used in the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { VisitType, useConfig } from '@openmrs/esm-framework';
import { ChartConfig } from '../../config-schema';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

export const useOfflineVisitType = () => {
const config = useConfig() as ChartConfig;
const { t } = useTranslation();
const [visitTypes, setVisitTypes] = useState<Array<VisitType>>([]);

useEffect(() => {
setVisitTypes([
{ uuid: config.offlineVisitTypeUuid, name: 'Offline Visit', display: t('offlineVisit', 'Offline Visit') },
]);
}, []);

return visitTypes;
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ import {
useVisitTypes,
useConfig,
useVisit,
useConnectivity,
} from '@openmrs/esm-framework';
import {
amPm,
convertTime12to24,
createOfflineVisitForPatient,
DefaultWorkspaceProps,
useActivePatientEnrollment,
} from '@openmrs/esm-patient-common-lib';
Expand All @@ -55,6 +57,7 @@ import BaseVisitType from './base-visit-type.component';
import LocationSelector from './location-selection.component';
import VisitAttributeTypeFields from './visit-attribute-type.component';
import styles from './visit-form.scss';
import { useOfflineVisitType } from '../hooks/useOfflineVisitType';

export type VisitFormData = {
visitDate: Date;
Expand All @@ -74,15 +77,17 @@ export type VisitFormData = {
const StartVisitForm: React.FC<DefaultWorkspaceProps> = ({ patientUuid, closeWorkspace, promptBeforeClosing }) => {
const { t } = useTranslation();
const isTablet = useLayoutType() === 'tablet';
const isOnline = useConnectivity();
const sessionUser = useSession();
const { error: errorFetchingLocations } = useLocations();
const { error: errorFetchingLocations } = isOnline ? useLocations() : { error: false };
const sessionLocation = sessionUser?.sessionLocation;
const config = useConfig() as ChartConfig;
const [contentSwitcherIndex, setContentSwitcherIndex] = useState(config.showRecommendedVisitTypeTab ? 0 : 1);
const [isSubmitting, setIsSubmitting] = useState(false);
const visitHeaderSlotState = useMemo(() => ({ patientUuid }), [patientUuid]);
const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid);
const allVisitTypes = useVisitTypes();
const allVisitTypes = isOnline ? useVisitTypes() : useOfflineVisitType();
const { mutate } = useVisit(patientUuid);
const { mutate: mutateVisit } = useVisit(patientUuid);
const [ignoreChanges, setIgnoreChanges] = useState(true);
const [errorFetchingResources, setErrorFetchingResources] = useState<{
Expand Down Expand Up @@ -170,6 +175,39 @@ const StartVisitForm: React.FC<DefaultWorkspaceProps> = ({ patientUuid, closeWor
};

const abortController = new AbortController();

if (!isOnline) {
createOfflineVisitForPatient(
patientUuid,
visitLocation.uuid,
config.offlineVisitTypeUuid,
payload.startDatetime,
).then(
(offlineVisit) => {
//setCurrentVisit(patientUuid, offlineVisit.uuid);
mutate();
closeWorkspace();
showToast({
critical: true,
kind: 'success',
description: t('visitStartedSuccessfully', '{visit} started successfully', {
visit: t('offlineVisit', 'Offline Visit'),
}),
title: t('visitStarted', 'Visit started'),
});
},
(error) => {
showNotification({
title: t('startVisitError', 'Error starting visit'),
kind: 'error',
critical: true,
description: error?.message,
});
},
);
return;
}

saveVisit(payload, abortController)
.pipe(first())
.subscribe(
Expand Down
5 changes: 3 additions & 2 deletions packages/esm-patient-common-lib/src/offline/visit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function useAutoCreatedOfflineVisit(patientUuid: string, offlineVisitType

useEffect(() => {
if (!isOnline && !isValidating && !currentVisit && !error) {
createOfflineVisitForPatient(patientUuid, location, offlineVisitTypeUuid).finally(() => mutate());
createOfflineVisitForPatient(patientUuid, location, offlineVisitTypeUuid, new Date()).finally(() => mutate());
}
}, [isOnline, currentVisit, isValidating, error, mutate, location, offlineVisitTypeUuid, patientUuid]);
}
Expand All @@ -100,6 +100,7 @@ export async function createOfflineVisitForPatient(
patientUuid: string,
location: string,
offlineVisitTypeUuid: string,
startDatetime: Date,
) {
const patientRegistrationSyncItems = await getSynchronizationItems<{ fhirPatient: fhir.Patient }>(
'patient-registration',
Expand All @@ -111,7 +112,7 @@ export async function createOfflineVisitForPatient(
const offlineVisit: OfflineVisit = {
uuid: uuid(),
patient: patientUuid,
startDatetime: new Date(),
startDatetime,
location,
visitType: offlineVisitTypeUuid,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function renderFormEntry() {
closeWorkspace: jest.fn(),
promptBeforeClosing: jest.fn(),
patientUuid: mockPatient.id,
formInfo: { formUuid: 'some-form-uuid' },
mutateForm: jest.fn(),
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import React, { useCallback, useMemo } from 'react';
import { useConfig, usePatient } from '@openmrs/esm-framework';
import { closeWorkspace, useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
import { Layer, Tile } from '@carbon/react';
import { useConfig, useConnectivity, usePatient } from '@openmrs/esm-framework';
import { EmptyDataIllustration, closeWorkspace, useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
import type { ConfigObject } from '../config-schema';
import FormsList from './forms-list.component';
import styles from './forms-dashboard.scss';
import { launchFormEntryOrHtmlForms } from '../form-entry-interop';
import { useForms } from '../hooks/use-forms';
import { useTranslation } from 'react-i18next';

const FormsDashboard = () => {
const { t } = useTranslation();
const config = useConfig<ConfigObject>();
const isOnline = useConnectivity();
const htmlFormEntryForms = config.htmlFormEntryForms;
const { patient, patientUuid } = usePatient();
const { data: forms, error, mutateForms } = useForms(patientUuid, undefined, undefined, undefined, config.orderBy);
const { data: forms, error, mutateForms } = useForms(patientUuid, undefined, undefined, !isOnline, config.orderBy);
const { currentVisit } = useVisitOrOfflineVisit(patientUuid);

function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) {
return isTablet ? <Layer>{children} </Layer> : <>{children}</>;
}

const handleFormOpen = useCallback(
(formUuid: string, encounterUuid: string, formName: string) => {
closeWorkspace('clinical-forms-workspace', true);
Expand All @@ -39,6 +47,17 @@ const FormsDashboard = () => {
}));
}, [config.formSections, forms]);

if (forms?.length === 0) {
return (
<ResponsiveWrapper isTablet>
<Tile className={styles.emptyState}>
<EmptyDataIllustration />
<p className={styles.emptyStateContent}>{t('noFormsToDisplay', 'There are no forms to display.')}</p>
</Tile>
</ResponsiveWrapper>
);
}

return (
<div className={styles.container}>
{sections.length === 0 ? (
Expand Down
15 changes: 15 additions & 0 deletions packages/esm-patient-forms-app/src/forms/forms-dashboard.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@import '~@openmrs/esm-styleguide/src/vars';

.emptyState {
text-align: center;
}

.emptyStateContent {
@include type.type-style("heading-compact-01");
color: $text-02;
margin-top: spacing.$spacing-05;
margin-bottom: spacing.$spacing-03;
}

// Desktop
:global(.omrs-breakpoint-gt-tablet) {
.container {
Expand Down
57 changes: 57 additions & 0 deletions packages/esm-patient-forms-app/src/forms/forms-dashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { useConfig } from '@openmrs/esm-framework';
import { useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
import { mockCurrentVisit } from '../__mocks__/visits.mock';
import FormsDashboard from './forms-dashboard.component';
import { useForms } from '../hooks/use-forms';

const mockUseConfig = useConfig as jest.Mock;
const mockUseVisitOrOfflineVisit = useVisitOrOfflineVisit as jest.Mock;

jest.mock('../hooks/use-forms', () => ({
useForms: jest.fn().mockReturnValueOnce({
data: [],
error: null,
isValidating: false,
allForms: [],
}),
}));

jest.mock('@openmrs/esm-framework', () => {
const originalModule = jest.requireActual('@openmrs/esm-framework');

return {
...originalModule,
useConfig: jest.fn(),
};
});

jest.mock('@openmrs/esm-patient-common-lib', () => {
const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib');

return {
...originalModule,
launchPatientWorkspace: jest.fn(),
useVisitOrOfflineVisit: jest.fn(),
};
});

describe('FormsDashboard', () => {
test('renders an empty state if there are no forms persisted on the server', async () => {
mockUseConfig.mockReturnValue({ htmlFormEntryForms: [] });

mockUseVisitOrOfflineVisit.mockReturnValue({
currentVisit: mockCurrentVisit,
error: null,
});

renderFormDashboard();

expect(screen.getByText(/there are no forms to display/i)).toBeInTheDocument();
});
});

function renderFormDashboard() {
render(<FormsDashboard />);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import debounce from 'lodash-es/debounce';
import fuzzy from 'fuzzy';
import { DataTableSkeleton, Layer, Tile } from '@carbon/react';
import { formatDatetime, useLayoutType } from '@openmrs/esm-framework';
import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
import FormsTable from './forms-table.component';
import styles from './forms-list.scss';
import type { CompletedFormInfo } from '../types';
Expand Down Expand Up @@ -84,14 +83,7 @@ const FormsList: React.FC<FormsListProps> = ({ completedForms, error, sectionNam
}

if (completedForms?.length === 0) {
return (
<ResponsiveWrapper isTablet>
<Tile className={styles.emptyState}>
<EmptyDataIllustration />
<p className={styles.emptyStateContent}>{t('noFormsToDisplay', 'There are no forms to display.')}</p>
</Tile>
</ResponsiveWrapper>
);
return <></>;
}

if (sectionName === 'forms') {
Expand Down
11 changes: 0 additions & 11 deletions packages/esm-patient-forms-app/src/forms/forms-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ beforeEach(async () => {
testProps.reset();
});

it('renders an empty state if there are no forms persisted on the server', async () => {
renderFormsList();

expect(screen.queryByRole('table')).not.toBeInTheDocument();
expect(screen.getByText(/there are no forms to display/i)).toBeInTheDocument();
});

it('renders a list of forms fetched from the server', async () => {
const user = userEvent.setup();
testProps.completedForms = forms.map((form) => ({ form, associatedEncounters: [] }));
Expand Down Expand Up @@ -52,10 +45,6 @@ it('renders a list of forms fetched from the server', async () => {
expect(within(screen.getByRole('table')).getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument(),
);

await user.type(searchbox, 'registration');

expect(screen.getByText(/No matching forms to display/i)).toBeInTheDocument();

await user.clear(searchbox);
await user.type(searchbox, 'lab');

Expand Down
Loading

0 comments on commit 881258a

Please sign in to comment.