Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'react-dates/initialize';
import moment from 'moment';
import { Content, Tile, Heading, Search, Dropdown, DatePicker, DatePickerInput } from '@carbon/react';
import 'moment/locale/fr';
import { createErrorHandler } from '@openmrs/esm-framework';
import { createErrorHandler, useLocations, useSession } from '@openmrs/esm-framework';
import Table from '../table/referrals-table.component';
import styles from './referrals-queue.scss';
import { getReferrals } from './referrals-queue.resource';
import { findNearestVisitLocationUuid, getReferrals } from './referrals-queue.resource';

export default function ReferralsQueue(props: ReferralsQueueProps) {
const [referrals, setReferrals]: [Referral[], Function] = React.useState([]);
Expand All @@ -19,6 +19,33 @@ export default function ReferralsQueue(props: ReferralsQueueProps) {
const [ptQuery, setPtQuery] = React.useState('');
const { t, i18n } = useTranslation();

const locations = useLocations('Visit Location');
const { sessionLocation } = useSession();
const [locationUuid, setLocationUuid] = React.useState<string>(undefined);

React.useEffect(() => {
if (locationUuid || !sessionLocation || locations.length === 0) {
return;
}

const visitLocationUuids = new Set(locations.map((location) => location.uuid));

if (visitLocationUuids.has(sessionLocation.uuid)) {
setLocationUuid(sessionLocation.uuid);
return;
}

let cancelled = false;
findNearestVisitLocationUuid(sessionLocation.uuid, visitLocationUuids).then((matchedUuid) => {
if (!cancelled && matchedUuid) {
setLocationUuid(matchedUuid);
}
});
return () => {
cancelled = true;
};
}, [locationUuid, sessionLocation, locations]);

const languageMatches = i18n.language?.match(/^(en|fr|ht).*/);
const language = (languageMatches && languageMatches[1]) || 'en';

Expand All @@ -27,15 +54,16 @@ export default function ReferralsQueue(props: ReferralsQueueProps) {
}, [language]);

React.useEffect(() => {
if (fromDate && toDate) {
if (fromDate && toDate && locationUuid) {
const sub = getReferrals({
fromDate: fromDate.format('YYYY-MM-DD'),
toDate: toDate.format('YYYY-MM-DD'),
locale: language,
locationUuid,
}).subscribe((referrals) => setReferrals(referrals), createErrorHandler());
return () => sub.unsubscribe();
}
}, [fromDate, toDate, language]);
}, [fromDate, toDate, language, locationUuid]);

const filteredReferrals = referrals
.filter(
Expand Down Expand Up @@ -74,6 +102,19 @@ export default function ReferralsQueue(props: ReferralsQueueProps) {
<DatePickerInput id="to-date" labelText={t('to', 'To')} />
</DatePicker>
</div>
<div className={styles.inputContainer}>
<div style={{ width: 400 }}>
<Dropdown
id="location"
label={t('select-location', 'Select Location')}
titleText={t('location', 'Location')}
items={locations}
itemToString={(location) => location?.display ?? ''}
selectedItem={locations.find((location) => location.uuid === locationUuid) ?? null}
onChange={(e) => setLocationUuid(e.selectedItem?.uuid)}
/>
</div>
</div>
<div className={styles.inputContainer}>
<div style={{ width: 400 }}>
<Dropdown
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { openmrsFetch, openmrsObservableFetch } from '@openmrs/esm-framework';
import { of } from 'rxjs';
import { findNearestVisitLocationUuid, getReferrals } from './referrals-queue.resource';

const mockedOpenmrsObservableFetch = openmrsObservableFetch as jest.Mock;
const mockedOpenmrsFetch = openmrsFetch as jest.Mock;

jest.mock('@openmrs/esm-framework', () => ({
...jest.requireActual('@openmrs/esm-framework'),
openmrsObservableFetch: jest.fn(),
openmrsFetch: jest.fn(),
}));

describe('getReferrals', () => {
beforeEach(() => {
mockedOpenmrsObservableFetch.mockReset();
mockedOpenmrsObservableFetch.mockReturnValue(of({ data: { dataSets: [{ rows: [] }] } }));
});

it('includes the given location in the report request', () => {
getReferrals({
fromDate: '2020-01-01',
toDate: '2020-02-01',
locale: 'en',
locationUuid: 'location-uuid-1',
}).subscribe();

expect(mockedOpenmrsObservableFetch).toHaveBeenCalledWith(expect.stringContaining('location=location-uuid-1'));
});
});

describe('findNearestVisitLocationUuid', () => {
beforeEach(() => {
mockedOpenmrsFetch.mockReset();
});

function mockLocationChain(chain: Record<string, string | undefined>) {
mockedOpenmrsFetch.mockImplementation((url: string) => {
const uuid = url.match(/\/location\/([^?]+)/)[1];
const parentUuid = chain[uuid];
return Promise.resolve({ data: { uuid, parentLocation: parentUuid ? { uuid: parentUuid } : undefined } });
});
}

it('resolves immediately without a network call when the starting location is already a candidate', async () => {
const result = await findNearestVisitLocationUuid('loc-a', new Set(['loc-a']));

expect(result).toBe('loc-a');
expect(mockedOpenmrsFetch).not.toHaveBeenCalled();
});

it('walks up one level to find a match', async () => {
mockLocationChain({ 'loc-room': 'loc-facility' });

const result = await findNearestVisitLocationUuid('loc-room', new Set(['loc-facility']));

expect(result).toBe('loc-facility');
expect(mockedOpenmrsFetch).toHaveBeenCalledWith(
expect.stringContaining('/location/loc-room?v=custom:(uuid,parentLocation:ref)'),
);
});

it('stops at the nearest match without walking past it', async () => {
mockLocationChain({
'loc-room': 'loc-department',
'loc-department': 'loc-facility',
'loc-facility': 'loc-country',
});

const result = await findNearestVisitLocationUuid('loc-room', new Set(['loc-facility', 'loc-country']));

expect(result).toBe('loc-facility');
expect(mockedOpenmrsFetch).toHaveBeenCalledTimes(2);
});

it('resolves undefined when no ancestor matches', async () => {
mockLocationChain({ 'loc-room': 'loc-facility' });

const result = await findNearestVisitLocationUuid('loc-room', new Set(['some-other-location']));

expect(result).toBeUndefined();
});

it('resolves undefined and does not hang on a cyclic hierarchy', async () => {
mockLocationChain({ 'loc-a': 'loc-b', 'loc-b': 'loc-a' });

const result = await findNearestVisitLocationUuid('loc-a', new Set(['some-other-location']));

expect(result).toBeUndefined();
expect(mockedOpenmrsFetch).toHaveBeenCalledTimes(2);
});

it('resolves undefined rather than throwing when a fetch fails', async () => {
mockedOpenmrsFetch.mockRejectedValueOnce(new Error('network error'));

await expect(findNearestVisitLocationUuid('loc-room', new Set(['loc-facility']))).resolves.toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,47 @@
import { openmrsObservableFetch } from '@openmrs/esm-framework';
import { openmrsFetch, openmrsObservableFetch } from '@openmrs/esm-framework';
import { type Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export function getReferrals({ fromDate, toDate, locale }): Observable<Object[]> {
export function getReferrals({ fromDate, toDate, locale, locationUuid }): Observable<Object[]> {
return openmrsObservableFetch(
`/ws/rest/v1/reportingrest/reportdata/cd7dfde7-764a-4da6-81c2-d5887ed1df51?startDate=${fromDate}&endDate=${toDate}&locale=${locale}`,
`/ws/rest/v1/reportingrest/reportdata/cd7dfde7-764a-4da6-81c2-d5887ed1df51?startDate=${fromDate}&endDate=${toDate}&locale=${locale}&location=${locationUuid}`,
).pipe(map(({ data }) => data['dataSets'][0]['rows']));
}

const LOCATION_ANCESTOR_REPRESENTATION = 'custom:(uuid,parentLocation:ref)';

/**
* Walks up the location hierarchy from `startingLocationUuid` (via
* `parentLocation`), one REST call per level, looking for the nearest
* location — starting with `startingLocationUuid` itself — whose uuid is a
* member of `candidateUuids`. Returns `undefined` if the walk reaches the
* root, hits a cycle, or a request fails. Zero network calls if
* `startingLocationUuid` is already a member of `candidateUuids`.
*/
export async function findNearestVisitLocationUuid(
startingLocationUuid: string | undefined,
candidateUuids: Set<string>,
): Promise<string | undefined> {
let currentUuid = startingLocationUuid;
const visited = new Set<string>();

while (currentUuid) {
if (candidateUuids.has(currentUuid)) {
return currentUuid;
}
if (visited.has(currentUuid)) {
return undefined;
}
visited.add(currentUuid);

try {
const { data } = await openmrsFetch(`/ws/rest/v1/location/${currentUuid}?v=${LOCATION_ANCESTOR_REPRESENTATION}`);
currentUuid = data?.parentLocation?.uuid;
} catch (e) {
console.error(e);
return undefined;
}
}

return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@ import { of } from 'rxjs';
import { render, fireEvent, screen, within, waitFor } from '@testing-library/react';
import MockDate from 'mockdate';
import ReferralsQueue from './referrals-queue.component';
import { getReferrals } from './referrals-queue.resource';
import { findNearestVisitLocationUuid, getReferrals } from './referrals-queue.resource';

const mockedUseConfig = jest.fn();
const mockedUseLocations = jest.fn();
const mockedUseSession = jest.fn();

jest.mock('./referrals-queue.resource');
jest.mock('@openmrs/esm-framework', () => ({
...jest.requireActual('@openmrs/esm-framework'),
useConfig: () => mockedUseConfig(),
useLocations: () => mockedUseLocations(),
useSession: () => mockedUseSession(),
}));

const mockedGetReferrals = getReferrals as jest.Mock;
const mockedFindNearestVisitLocationUuid = findNearestVisitLocationUuid as jest.Mock;

const locations = [
{ uuid: 'location-uuid-1', display: 'Mirebalais Hospital' },
{ uuid: 'location-uuid-2', display: 'Cange Clinic' },
];

const referrals = [
{
Expand Down Expand Up @@ -62,6 +72,8 @@ window.location = { href: '/referrals-queue' };

describe('referrals queue', () => {
const todayString = '2020-10-31';
let view: ReturnType<typeof render>;

beforeAll(() => {
mockedUseConfig.mockReturnValue({
links: {
Expand All @@ -79,7 +91,11 @@ describe('referrals queue', () => {
MockDate.set(todayString + 'T10:00:00.000-0400');
mockedGetReferrals.mockReset();
mockedGetReferrals.mockReturnValue(of(referrals));
render(<ReferralsQueue />);
mockedUseLocations.mockReturnValue(locations);
mockedUseSession.mockReturnValue({ sessionLocation: locations[0] });
mockedFindNearestVisitLocationUuid.mockReset();
mockedFindNearestVisitLocationUuid.mockResolvedValue(undefined);
view = render(<ReferralsQueue />);
});

afterEach(() => {
Expand Down Expand Up @@ -166,6 +182,42 @@ describe('referrals queue', () => {
expect(screen.queryByText('Test Referral Type', { selector: 'span' })).not.toBeNull();
});

it('defaults the location dropdown to the session location', () => {
screen.getByText(locations[0].display, { selector: 'span' });
});

it('resolves the location dropdown to the nearest visit-location ancestor when the session location is a descendant', async () => {
view.unmount();
const deskLocation = { uuid: 'desk-uuid', display: 'Front Desk' };
mockedUseSession.mockReturnValue({ sessionLocation: deskLocation });
mockedFindNearestVisitLocationUuid.mockResolvedValue(locations[1].uuid);

render(<ReferralsQueue />);

await waitFor(() => {
screen.getByText(locations[1].display, { selector: 'span' });
});

expect(mockedFindNearestVisitLocationUuid).toHaveBeenCalledWith(
deskLocation.uuid,
new Set(locations.map((location) => location.uuid)),
);
});

it('refetches referrals for the newly selected location', () => {
fireEvent.click(
screen.getByLabelText('Location', {
selector: 'button',
}),
);
fireEvent.click(
screen.getByText(locations[1].display, {
selector: 'div',
}),
);
expect(mockedGetReferrals).toHaveBeenLastCalledWith(expect.objectContaining({ locationUuid: locations[1].uuid }));
});

it('filters by statuses, with dropdown inferred from data', () => {
fireEvent.click(
screen.getByLabelText('Status', {
Expand Down
2 changes: 2 additions & 0 deletions packages/esm-referrals-queue-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"emr-id": "EMR ID",
"filter-by-patient": "Filter by patient",
"from": "From",
"location": "Location",
"name": "Name",
"referral-date": "Referral Date",
"referral-type": "Referral Type",
"referrals-queue": "Referrals Queue",
"select-location": "Select Location",
"select-referral-type": "Select Referral Type",
"select-status": "Select Status",
"status": "Status",
Expand Down
Loading