Skip to content

Commit 8e6360b

Browse files
authored
U4X-1008 : Action end visit button does not actually remove patient from queue of the user that has ended the visit (#321)
* add end visit button and dialog * add submission code * fix end visit * fix unending loading * chore * make search dialog disappear * fix build error
1 parent c63d5c0 commit 8e6360b

11 files changed

+704
-457
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { showModal } from '@openmrs/esm-framework';
2+
import React, { useCallback } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
5+
interface EndVisitActionButtonProps {
6+
patientUuid: string;
7+
}
8+
9+
const EndVisitActionButton: React.FC<EndVisitActionButtonProps> = ({ patientUuid }) => {
10+
const { t } = useTranslation();
11+
12+
const launchEndVisitModal = useCallback(() => {
13+
const dispose = showModal('end-visit-modal', {
14+
patientUuid,
15+
closeModal: () => dispose(),
16+
});
17+
}, [patientUuid]);
18+
19+
return (
20+
<li className="cds--overflow-menu-options__option">
21+
<button
22+
className="cds--overflow-menu-options__btn"
23+
role="menuitem"
24+
title={t('endAVisit', 'End a visit')}
25+
data-floating-menu-primary-focus
26+
onClick={() => launchEndVisitModal()}
27+
style={{
28+
maxWidth: '100vw',
29+
}}
30+
>
31+
<span className="cds--overflow-menu-options__option-content">{t('endAVisit', 'End a visit')}</span>
32+
</button>
33+
</li>
34+
);
35+
};
36+
37+
export default EndVisitActionButton;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import { Button, Form, ModalBody, ModalFooter, ModalHeader, InlineLoading } from '@carbon/react';
3+
import styles from './end-visit-modal.scss';
4+
import { useTranslation } from 'react-i18next';
5+
import {
6+
getCoreTranslation,
7+
getSessionStore,
8+
navigate,
9+
parseDate,
10+
restBaseUrl,
11+
showNotification,
12+
showSnackbar,
13+
useSession,
14+
useVisit,
15+
} from '@openmrs/esm-framework';
16+
import {
17+
getCareProvider,
18+
getCurrentPatientQueueByPatientUuid,
19+
updateQueueEntry,
20+
updateVisit,
21+
} from '../patient-queues.resource';
22+
import { QueueStatus, extractErrorMessagesFromResponse, handleMutate } from '../../utils/utils';
23+
24+
interface EndVisitConfirmationProps {
25+
patientUuid: string;
26+
closeModal: () => void;
27+
}
28+
29+
const EndVisitConfirmation: React.FC<EndVisitConfirmationProps> = ({ closeModal, patientUuid }) => {
30+
const { t } = useTranslation();
31+
32+
const [isFetchingProvider, setIsFetchingProvider] = useState(false);
33+
34+
const [isEndingVisit, setIsEndingVisit] = useState(false);
35+
36+
const priorityLabels = useMemo(() => ['Not Urgent', 'Urgent', 'Emergency'], []);
37+
38+
const [provider, setProvider] = useState('');
39+
40+
const { activeVisit } = useVisit(patientUuid);
41+
42+
const sessionUser = useSession();
43+
44+
// Memoize the function to fetch the provider using useCallback
45+
const fetchProvider = useCallback(() => {
46+
if (!sessionUser?.user?.uuid) return;
47+
48+
setIsFetchingProvider(true);
49+
50+
getCareProvider(sessionUser?.user?.uuid).then(
51+
(response) => {
52+
const uuid = response?.data?.results[0].uuid;
53+
setIsFetchingProvider(false);
54+
setProvider(uuid);
55+
},
56+
(error) => {
57+
const errorMessages = extractErrorMessagesFromResponse(error);
58+
setIsFetchingProvider(false);
59+
showNotification({
60+
title: "Couldn't get provider",
61+
kind: 'error',
62+
critical: true,
63+
description: errorMessages.join(','),
64+
});
65+
},
66+
);
67+
}, [sessionUser?.user?.uuid]);
68+
69+
useEffect(() => fetchProvider(), [fetchProvider]);
70+
71+
const handleEndVisit = async () => {
72+
setIsEndingVisit(true);
73+
74+
const endVisitPayload = {
75+
location: activeVisit?.location?.uuid,
76+
startDatetime: parseDate(activeVisit?.startDatetime),
77+
visitType: activeVisit?.visitType?.uuid,
78+
stopDatetime: new Date(),
79+
};
80+
81+
try {
82+
let hasEndedVisit = false;
83+
let hasEndedQueue = false;
84+
let queueEntry;
85+
86+
// 1. Attempt to end the visit if it exists
87+
if (activeVisit?.uuid) {
88+
const visitResponse = await updateVisit(activeVisit.uuid, endVisitPayload);
89+
if (visitResponse.status === 200) {
90+
hasEndedVisit = true;
91+
}
92+
}
93+
94+
// 2. Get queue entry and end it if found
95+
const queueResponse = await getCurrentPatientQueueByPatientUuid(patientUuid, sessionUser?.sessionLocation?.uuid);
96+
97+
const queues = queueResponse?.data?.results?.[0]?.patientQueues || [];
98+
queueEntry = queues.find((item) => item?.patient?.uuid === patientUuid);
99+
100+
if (queueEntry) {
101+
await updateQueueEntry(QueueStatus.Completed, provider, queueEntry.uuid, 0, priorityLabels[0], 'visit-ended');
102+
hasEndedQueue = true;
103+
}
104+
105+
// 3. If anything was ended, proceed with navigation and feedback
106+
if (hasEndedVisit || hasEndedQueue) {
107+
let navigateTo = `${window.getOpenmrsSpaBase()}home`;
108+
109+
if (queueEntry) {
110+
const roles = getSessionStore().getState().session?.user?.roles || [];
111+
const hasClinicianRole = roles.some((role) => role?.display === 'Organizational: Clinician');
112+
const hasTriageRole = roles.some((role) => role?.display === 'Triage');
113+
114+
if (hasClinicianRole) {
115+
navigateTo = `${window.getOpenmrsSpaBase()}home/clinical-room-patient-queues`;
116+
} else if (hasTriageRole) {
117+
navigateTo = `${window.getOpenmrsSpaBase()}home/triage-patient-queues`;
118+
}
119+
}
120+
121+
closeModal();
122+
navigate({ to: navigateTo });
123+
handleMutate(`${restBaseUrl}/patientqueue`);
124+
setIsEndingVisit(false);
125+
126+
showSnackbar({
127+
title: hasEndedVisit ? 'Visit Ended' : 'Queue Completed',
128+
subtitle: t(
129+
hasEndedVisit && hasEndedQueue ? 'endedSuccessfully' : hasEndedVisit ? 'visitEndedOnly' : 'queueEndedOnly',
130+
hasEndedVisit && hasEndedQueue
131+
? 'Visit and queue ended successfully'
132+
: hasEndedVisit
133+
? 'Visit ended successfully'
134+
: 'Queue ended successfully',
135+
),
136+
kind: 'success',
137+
});
138+
} else {
139+
// Nothing was ended
140+
closeModal();
141+
setIsEndingVisit(false);
142+
showSnackbar({
143+
title: 'No Action Taken',
144+
subtitle: t('noVisitOrQueueToEnd', 'No active visit or queue found to end.'),
145+
kind: 'info',
146+
});
147+
}
148+
} catch (error) {
149+
closeModal();
150+
setIsEndingVisit(false);
151+
const errorMessages = extractErrorMessagesFromResponse(error);
152+
showNotification({
153+
title: t('endVisit', 'Error ending visit'),
154+
kind: 'error',
155+
critical: true,
156+
description: errorMessages.join(','),
157+
});
158+
}
159+
};
160+
161+
return (
162+
<Form>
163+
{isFetchingProvider && <InlineLoading status="active" description="Is Fetching" />}
164+
<ModalHeader closeModal={close} className={styles.modalHeader}>
165+
{t('endVisit', 'End Visit')}?
166+
</ModalHeader>
167+
<ModalBody>
168+
<p className={styles.bodyText}>
169+
{t('endVisitText', `Are you sure you want to end this visit? This action can't be undone.`)}
170+
</p>
171+
</ModalBody>
172+
<ModalFooter>
173+
<Button size="lg" kind="secondary" onClick={closeModal}>
174+
{getCoreTranslation('cancel')}
175+
</Button>
176+
<Button autoFocus kind="danger" onClick={handleEndVisit} size="lg" disabled={isEndingVisit}>
177+
{isEndingVisit ? (
178+
<InlineLoading description={t('endingVisit', 'Ending visit...')} />
179+
) : (
180+
t('endAVisit', 'End a visit')
181+
)}
182+
</Button>
183+
</ModalFooter>
184+
</Form>
185+
);
186+
};
187+
188+
export default EndVisitConfirmation;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@use '@carbon/layout';
2+
@use '@carbon/type';
3+
@use '@openmrs/esm-styleguide/src/vars' as *;
4+
5+
.bodyText {
6+
@include type.type-style('body-long-01');
7+
}
8+
9+
.modalHeader {
10+
@include type.type-style('heading-compact-02');
11+
}

packages/esm-patient-queues-app/src/active-visits/start-visit-form-button.component.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
import React, { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import StartVisitForm from '../components/visit-form/visit-form.component';
3+
import StartVisitForm from '../components/visit-form/start-a-visit-form.workspace';
44
import { Button } from '@carbon/react';
5+
import { launchWorkspace } from '@openmrs/esm-framework';
56

67
interface StartVisitFormProps {
7-
patientUuid?: string;
8+
patientUuid: string;
89
}
910

1011
const StartVisitButton: React.FC<StartVisitFormProps> = ({ patientUuid }) => {
1112
const { t } = useTranslation();
12-
const [showOverlay, setShowOverlay] = useState(false);
13-
const handleClick = () => {
14-
setShowOverlay(true);
15-
};
1613

14+
const handleLaunchWorkspace = () => {
15+
launchWorkspace('start-visit-form-workspace', {
16+
patientUuid,
17+
});
18+
};
1719
return (
18-
<>
19-
<Button onClick={handleClick}>{t('startAVisit', 'Start a Visit')}</Button>
20-
{showOverlay && (
21-
<StartVisitForm
22-
header={t('startAVisit', 'Start a Visit')}
23-
closePanel={() => setShowOverlay(false)}
24-
patientUuid={patientUuid}
25-
/>
26-
)}
27-
</>
20+
<Button onClick={handleLaunchWorkspace} aria-label={t('startAVisit', 'Start a Visit')}>
21+
{t('startAVisit', 'Start a Visit')}
22+
</Button>
2823
);
2924
};
3025

0 commit comments

Comments
 (0)