diff --git a/packages/esm-patient-chart-app/src/actions-buttons/start-visit.component.tsx b/packages/esm-patient-chart-app/src/actions-buttons/start-visit.component.tsx index 806b76116d..08f57f984c 100644 --- a/packages/esm-patient-chart-app/src/actions-buttons/start-visit.component.tsx +++ b/packages/esm-patient-chart-app/src/actions-buttons/start-visit.component.tsx @@ -14,7 +14,9 @@ const StartVisitOverflowMenuItem: React.FC = ({ const { currentVisit } = useVisit(patient?.id); const isDeceased = Boolean(patient?.deceasedDateTime); - const handleLaunchModal = useCallback(() => launchPatientWorkspace('start-visit-workspace-form'), []); + const handleLaunchModal = useCallback(() => launchPatientWorkspace('start-visit-workspace-form', { + openedFrom: "patient-chart-start-visit", + }), []); return ( !currentVisit && diff --git a/packages/esm-patient-chart-app/src/actions-buttons/start-visit.test.tsx b/packages/esm-patient-chart-app/src/actions-buttons/start-visit.test.tsx index 932dbc6735..37d70d604c 100644 --- a/packages/esm-patient-chart-app/src/actions-buttons/start-visit.test.tsx +++ b/packages/esm-patient-chart-app/src/actions-buttons/start-visit.test.tsx @@ -38,7 +38,9 @@ describe('StartVisitOverflowMenuItem', () => { await user.click(startVisitButton); expect(launchPatientWorkspace).toHaveBeenCalledTimes(1); - expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form'); + expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form', { + openedFrom: 'patient-chart-start-visit', + }); }); it('should not show start visit button for a deceased patient', () => { diff --git a/packages/esm-patient-chart-app/src/config-schema.ts b/packages/esm-patient-chart-app/src/config-schema.ts index ee3158d05e..e21dac645f 100644 --- a/packages/esm-patient-chart-app/src/config-schema.ts +++ b/packages/esm-patient-chart-app/src/config-schema.ts @@ -128,11 +128,6 @@ export const esmPatientChartSchema = { _default: '159947AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', _type: Type.ConceptUuid, }, - visitQueueNumberAttributeUuid: { - _type: Type.ConceptUuid, - _description: 'The UUID of the visit attribute that contains the visit queue number.', - _default: 'c61ce16f-272a-41e7-9924-4c555d0932c5', - }, visitTypeResourceUrl: { _type: Type.String, _default: '/etl-latest/etl/patient/', @@ -156,9 +151,8 @@ export interface ChartConfig { showAllEncountersTab: boolean; showExtraVisitAttributesSlot: boolean; showRecommendedVisitTypeTab: boolean; - showServiceQueueFields: boolean; - showUpcomingAppointments: boolean; - visitQueueNumberAttributeUuid: string; + showServiceQueueFields: boolean; // used by extension from esm-service-queues-app + showUpcomingAppointments: boolean; // used by extension from esm-appointments-app visitTypeResourceUrl: string; visitAttributeTypes: Array<{ displayInThePatientBanner: boolean; diff --git a/packages/esm-patient-chart-app/src/routes.json b/packages/esm-patient-chart-app/src/routes.json index 99417cfb16..5973d07f33 100644 --- a/packages/esm-patient-chart-app/src/routes.json +++ b/packages/esm-patient-chart-app/src/routes.json @@ -122,6 +122,7 @@ }, { "name": "start-visit-workspace-form", + "slot": "start-visit-workspace-form-slot", "component": "startVisitForm", "meta": { "title": "Start a visit" diff --git a/packages/esm-patient-chart-app/src/visit-header/visit-header.component.tsx b/packages/esm-patient-chart-app/src/visit-header/visit-header.component.tsx index dcbe615505..f91a20d637 100644 --- a/packages/esm-patient-chart-app/src/visit-header/visit-header.component.tsx +++ b/packages/esm-patient-chart-app/src/visit-header/visit-header.component.tsx @@ -118,7 +118,7 @@ const PatientInfo: React.FC = ({ patient }) => { }; function launchStartVisitForm() { - launchPatientWorkspace('start-visit-workspace-form'); + launchPatientWorkspace('start-visit-workspace-form', {openedFrom: "patient-chart-start-visit"}); } const VisitHeader: React.FC<{ patient: fhir.Patient }> = ({ patient }) => { diff --git a/packages/esm-patient-chart-app/src/visit-header/visit-header.test.tsx b/packages/esm-patient-chart-app/src/visit-header/visit-header.test.tsx index d715f721cd..3ba87bc084 100644 --- a/packages/esm-patient-chart-app/src/visit-header/visit-header.test.tsx +++ b/packages/esm-patient-chart-app/src/visit-header/visit-header.test.tsx @@ -84,7 +84,9 @@ describe('Visit header', () => { expect(startVisitButton).toBeInTheDocument(); await user.click(startVisitButton); - expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form'); + expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form', { + openedFrom: 'patient-chart-start-visit', + }); }); test('should display a truncated name when the patient name is very long', async () => { diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useMutateAppointments.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useMutateAppointments.tsx deleted file mode 100644 index 6d7f545a3e..0000000000 --- a/packages/esm-patient-chart-app/src/visit/hooks/useMutateAppointments.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useCallback } from 'react'; -import { useSWRConfig } from 'swr'; -import { restBaseUrl } from '@openmrs/esm-framework'; - -// this is copied directly from a similar hook in the appointments-app in patient management; ideally at some point we could import that hook directly -const appointmentUrlMatcher = `${restBaseUrl}/appointment`; - -export function useMutateAppointments() { - const { mutate } = useSWRConfig(); - // this mutate is intentionally broad because there may be many different keys that need to be invalidated when appointments are updated - const mutateAppointments = useCallback( - () => - mutate((key) => { - return ( - (typeof key === 'string' && key.startsWith(appointmentUrlMatcher)) || - (Array.isArray(key) && key[0].startsWith(appointmentUrlMatcher)) - ); - }), - [mutate], - ); - - return { - mutateAppointments, - }; -} diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useServiceQueue.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useServiceQueue.tsx index 04786dbab7..2d4afa3874 100644 --- a/packages/esm-patient-chart-app/src/visit/hooks/useServiceQueue.tsx +++ b/packages/esm-patient-chart-app/src/visit/hooks/useServiceQueue.tsx @@ -1,61 +1,5 @@ import { openmrsFetch, restBaseUrl, toDateObjectStrict, toOmrsIsoString } from '@openmrs/esm-framework'; -export async function saveQueueEntry( - visitUuid: string, - queueUuid: string, - patientUuid: string, - priority: string, - status: string, - sortWeight: number, - locationUuid: string, - visitQueueNumberAttributeUuid: string, - abortController?: AbortController, -) { - await Promise.all([ - generateVisitQueueNumber(locationUuid, visitUuid, queueUuid, visitQueueNumberAttributeUuid, abortController), - ]); - - return openmrsFetch(`${restBaseUrl}/visit-queue-entry`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: { - visit: { uuid: visitUuid }, - queueEntry: { - status: { - uuid: status, - }, - priority: { - uuid: priority, - }, - queue: { - uuid: queueUuid, - }, - patient: { - uuid: patientUuid, - }, - startedAt: toDateObjectStrict(toOmrsIsoString(new Date())), - sortWeight: sortWeight, - }, - }, - signal: abortController?.signal, - }); -} - -export async function generateVisitQueueNumber( - location: string, - visitUuid: string, - queueUuid: string, - visitQueueNumberAttributeUuid: string, - abortController?: AbortController, -) { - await openmrsFetch( - `${restBaseUrl}/queue-entry-number?location=${location}&queue=${queueUuid}&visit=${visitUuid}&visitAttributeType=${visitQueueNumberAttributeUuid}`, - { signal: abortController?.signal }, - ); -} - export function removeQueuedPatient( queueUuid: string, queueEntryUuid: string, diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx deleted file mode 100644 index 98553c980b..0000000000 --- a/packages/esm-patient-chart-app/src/visit/hooks/useUpcomingAppointments.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import dayjs from 'dayjs'; -import { openmrsFetch, restBaseUrl, type OpenmrsResource, useAbortController } from '@openmrs/esm-framework'; -import { omrsDateFormat } from '../../constants'; - -export interface AppointmentPayload { - patientUuid: string; - serviceUuid: string; - startDateTime: string; - endDateTime: string; - appointmentKind: string; - providers?: Array; - locationUuid: string; - comments?: string; - status?: string; - appointmentNumber?: string; - uuid?: string; - providerUuid?: string | OpenmrsResource; - dateHonored?: string; -} - -export const updateAppointmentStatus = async ( - toStatus: string, - appointmentUuid: string, - abortController?: AbortController, -) => { - const statusChangeTime = dayjs().format(omrsDateFormat); - const url = `${restBaseUrl}/appointments/${appointmentUuid}/status-change`; - return await openmrsFetch(url, { - body: { toStatus, onDate: statusChangeTime }, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - signal: abortController?.signal, - }); -}; diff --git a/packages/esm-patient-chart-app/src/visit/start-visit-button.component.tsx b/packages/esm-patient-chart-app/src/visit/start-visit-button.component.tsx index e1cc24e514..6f88435e10 100644 --- a/packages/esm-patient-chart-app/src/visit/start-visit-button.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/start-visit-button.component.tsx @@ -1,8 +1,7 @@ -import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { Button } from '@carbon/react'; import { launchPatientChartWithWorkspaceOpen } from '@openmrs/esm-patient-common-lib'; -import { navigate } from '@openmrs/esm-framework'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; const StartVisitButton = ({ patientUuid }) => { const { t } = useTranslation(); @@ -11,6 +10,9 @@ const StartVisitButton = ({ patientUuid }) => { launchPatientChartWithWorkspaceOpen({ patientUuid, workspaceName: 'start-visit-workspace-form', + additionalProps: { + openedFrom: 'patient-chart-start-visit' + } }); }, [patientUuid]); diff --git a/packages/esm-patient-chart-app/src/visit/visit-action-items/edit-visit-details.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-action-items/edit-visit-details.component.tsx index 29dce5354a..6d96a877e4 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-action-items/edit-visit-details.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-action-items/edit-visit-details.component.tsx @@ -18,6 +18,7 @@ const EditVisitDetailsActionItem: React.FC = ({ launchPatientWorkspace('start-visit-workspace-form', { workspaceTitle: t('editVisitDetails', 'Edit visit details'), visitToEdit: visit, + openedFrom: 'patient-chart-edit-visit' }); }; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx index f9be291f36..ed6147be60 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx @@ -1,7 +1,3 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import classNames from 'classnames'; -import dayjs from 'dayjs'; -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import { Button, ButtonSet, @@ -16,18 +12,12 @@ import { Stack, Switch, } from '@carbon/react'; -import { Controller, FormProvider, useForm } from 'react-hook-form'; -import { first } from 'rxjs/operators'; -import { from } from 'rxjs'; -import { useTranslation } from 'react-i18next'; -import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { + Extension, ExtensionSlot, formatDatetime, type NewVisitPayload, - openmrsFetch, - restBaseUrl, saveVisit, showSnackbar, toDateObjectStrict, @@ -35,13 +25,12 @@ import { updateVisit, useConfig, useConnectivity, + useFeatureFlag, useLayoutType, usePatient, useSession, useVisit, - useVisitTypes, type Visit, - useFeatureFlag, } from '@openmrs/esm-framework'; import { convertTime12to24, @@ -50,27 +39,45 @@ import { time12HourFormatRegex, useActivePatientEnrollment, } from '@openmrs/esm-patient-common-lib'; +import classNames from 'classnames'; +import dayjs from 'dayjs'; +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { type ChartConfig } from '../../config-schema'; -import { type VisitFormData } from './visit-form.resource'; -import { MemoizedRecommendedVisitType } from './recommended-visit-type.component'; -import { saveQueueEntry } from '../hooks/useServiceQueue'; -import { updateAppointmentStatus } from '../hooks/useUpcomingAppointments'; -import { useMutateAppointments } from '../hooks/useMutateAppointments'; -import { useOfflineVisitType } from '../hooks/useOfflineVisitType'; + +import { useDefaultVisitLocation } from '../hooks/useDefaultVisitLocation'; +import { useEmrConfiguration } from '../hooks/useEmrConfiguration'; import { useVisitAttributeTypes } from '../hooks/useVisitAttributeType'; -import { useVisitQueueEntry } from '../queue-entry/queue.resource'; import { useInfiniteVisits, useVisits } from '../visits-widget/visit.resource'; import BaseVisitType from './base-visit-type.component'; import LocationSelector from './location-selector.component'; +import { MemoizedRecommendedVisitType } from './recommended-visit-type.component'; import VisitAttributeTypeFields from './visit-attribute-type.component'; import VisitDateTimeField from './visit-date-time.component'; +import { + createVisitAttribute, + deleteVisitAttribute, + type OnVisitCreatedOrUpdatedCallback, + updateVisitAttribute, + useConditionalVisitTypes, + useOnVisitCreatedOrUpdatedCallbacks, + type VisitFormData, +} from './visit-form.resource'; import styles from './visit-form.scss'; -import { useDefaultVisitLocation } from '../hooks/useDefaultVisitLocation'; -import { useEmrConfiguration } from '../hooks/useEmrConfiguration'; dayjs.extend(isSameOrBefore); interface StartVisitFormProps extends DefaultPatientWorkspaceProps { + /** + * A unique string identifying where the visit form is opened from. + * This string is passed into various extensions within the form to + * affect how / if they should be rendered. + */ + openedFrom: string; + showPatientHeader?: boolean; showVisitEndDateTimeFields: boolean; visitToEdit?: Visit; @@ -83,6 +90,7 @@ const StartVisitForm: React.FC = ({ showPatientHeader = false, showVisitEndDateTimeFields, visitToEdit, + openedFrom, }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; @@ -104,28 +112,15 @@ const StartVisitForm: React.FC = ({ const { mutate: mutateCurrentVisit } = useVisit(patientUuid); const { mutateVisits } = useVisits(patientUuid); const { mutateVisits: mutateInfiniteVisits } = useInfiniteVisits(patientUuid); - const { mutateAppointments } = useMutateAppointments(); const allVisitTypes = useConditionalVisitTypes(); - const { mutate } = useVisit(patientUuid); const [errorFetchingResources, setErrorFetchingResources] = useState<{ blockSavingForm: boolean; }>(null); - const [upcomingAppointment, setUpcomingAppointment] = useState(null); - const upcomingAppointmentState = useMemo(() => ({ patientUuid, setUpcomingAppointment }), [patientUuid]); - const visitQueueNumberAttributeUuid = config.visitQueueNumberAttributeUuid; - const [visitUuid, setVisitUuid] = useState(''); - const { mutate: mutateQueueEntry } = useVisitQueueEntry(patientUuid, visitUuid); const { visitAttributeTypes } = useVisitAttributeTypes(); const [extraVisitInfo, setExtraVisitInfo] = useState(null); - const [{ service, priority, status, sortWeight, queueLocation }, setVisitFormFields] = useState({ - service: null, - priority: null, - status: null, - sortWeight: null, - queueLocation: null, - }); + const [OnVisitCreatedOrUpdatedCallbacks, setOnVisitCreatedOrUpdatedCallbacks] = useOnVisitCreatedOrUpdatedCallbacks(); const displayVisitStopDateTimeFields = useMemo( () => Boolean(visitToEdit?.uuid || showVisitEndDateTimeFields), [visitToEdit?.uuid, showVisitEndDateTimeFields], @@ -358,11 +353,7 @@ const StartVisitForm: React.FC = ({ if (value) { // Update attribute with new value promises.push( - openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${attributeToEdit.uuid}`, { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { value }, - }).catch((err) => { + updateVisitAttribute(visitUuid, attributeToEdit.uuid, value).catch((err) => { showSnackbar({ title: t('errorUpdatingVisitAttribute', 'Error updating the {{attributeName}} visit attribute', { attributeName: attributeToEdit.attributeType.display, @@ -371,14 +362,13 @@ const StartVisitForm: React.FC = ({ isLowContrast: false, subtitle: err?.message, }); + return Promise.reject(err); // short-circuit promise chain }), ); } else { // Delete attribute if no value is provided promises.push( - openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${attributeToEdit.uuid}`, { - method: 'DELETE', - }).catch((err) => { + deleteVisitAttribute(visitUuid, attributeToEdit.uuid).catch((err) => { showSnackbar({ title: t('errorDeletingVisitAttribute', 'Error deleting the {{attributeName}} visit attribute', { attributeName: attributeToEdit.attributeType.display, @@ -387,6 +377,7 @@ const StartVisitForm: React.FC = ({ isLowContrast: false, subtitle: err?.message, }); + return Promise.reject(err); // short-circuit promise chain }), ); } @@ -394,11 +385,7 @@ const StartVisitForm: React.FC = ({ } else { if (value) { promises.push( - openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute`, { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { attributeType, value }, - }).catch((err) => { + createVisitAttribute(visitUuid, attributeType, value).catch((err) => { showSnackbar({ title: t('errorCreatingVisitAttribute', 'Error creating the {{attributeName}} visit attribute', { attributeName: visitAttributeTypes?.find((type) => type.uuid === attributeType)?.display, @@ -407,6 +394,7 @@ const StartVisitForm: React.FC = ({ isLowContrast: false, subtitle: err?.message, }); + return Promise.reject(err); // short-circuit promise chain }), ); } @@ -436,8 +424,6 @@ const StartVisitForm: React.FC = ({ visitStopTimeFormat, } = data; - setIsSubmitting(true); - const [hours, minutes] = convertTime12to24(visitStartTime, visitStartTimeFormat); let payload: NewVisitPayload = { @@ -489,115 +475,29 @@ const StartVisitForm: React.FC = ({ handleCreateExtraVisitInfo && handleCreateExtraVisitInfo(); } + setIsSubmitting(true); if (isOnline) { - (visitToEdit?.uuid + const visitRequest = visitToEdit?.uuid ? updateVisit(visitToEdit?.uuid, payload, abortController) - : saveVisit(payload, abortController) - ) - .then((response) => { - if (config.showServiceQueueFields && queueLocation && service && priority) { - // retrieve values from the queue extension - setVisitUuid(response.data.uuid); - - saveQueueEntry( - response.data.uuid, - service, - patientUuid, - priority, - status, - sortWeight, - queueLocation, - visitQueueNumberAttributeUuid, - abortController, - ).then( - () => { - mutateCurrentVisit(); - mutateVisits(); - mutateInfiniteVisits(); - mutateQueueEntry(); - showSnackbar({ - kind: 'success', - title: t('visitStarted', 'Visit started'), - subtitle: t('queueAddedSuccessfully', `Patient added to the queue successfully.`), - }); - }, - (error) => { - showSnackbar({ - title: t('queueEntryError', 'Error adding patient to the queue'), - kind: 'error', - isLowContrast: false, - subtitle: error?.message, - }); - }, - ); - - if (config.showUpcomingAppointments && upcomingAppointment) { - updateAppointmentStatus('CheckedIn', upcomingAppointment.uuid, abortController).then( - () => { - mutateCurrentVisit(); - mutateVisits(); - mutateInfiniteVisits(); - mutateAppointments(); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t('appointmentMarkedChecked', 'Appointment marked as Checked In'), - title: t('appointmentCheckedIn', 'Appointment Checked In'), - }); - }, - (error) => { - showSnackbar({ - title: t('updateError', 'Error updating upcoming appointment'), - kind: 'error', - isLowContrast: false, - subtitle: error?.message, - }); - }, - ); - } - } + : saveVisit(payload, abortController); - // TODO: Refactor this to use Promises - from(handleVisitAttributes(visitAttributes, response.data.uuid)) - .pipe(first()) - .subscribe({ - next: (attributesResponses) => { - setIsSubmitting(false); - // Check for no undefined, - // that if there was no failed requests on either creating, updating or deleting an attribute - // then continue and close workspace - if (!attributesResponses.includes(undefined)) { - mutateCurrentVisit(); - mutateVisits(); - mutateInfiniteVisits(); - closeWorkspace({ ignoreChanges: true }); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: !visitToEdit - ? t('visitStartedSuccessfully', '{{visit}} started successfully', { - visit: response?.data?.visitType?.display ?? t('visit', 'Visit'), - }) - : t('visitDetailsUpdatedSuccessfully', '{{visit}} updated successfully', { - visit: response?.data?.visitType?.display ?? t('pastVisit', 'Past visit'), - }), - title: !visitToEdit - ? t('visitStarted', 'Visit started') - : t('visitDetailsUpdated', 'Visit details updated'), - }); - } - }, - error: (error) => { - showSnackbar({ - title: !visitToEdit - ? t('startVisitError', 'Error starting visit') - : t('errorUpdatingVisitDetails', 'Error updating visit details'), - kind: 'error', - isLowContrast: false, - subtitle: error?.message, - }); - }, - }); + visitRequest + .then((response) => { + showSnackbar({ + isLowContrast: true, + kind: 'success', + subtitle: !visitToEdit + ? t('visitStartedSuccessfully', '{{visit}} started successfully', { + visit: response?.data?.visitType?.display ?? t('visit', 'Visit'), + }) + : t('visitDetailsUpdatedSuccessfully', '{{visit}} updated successfully', { + visit: response?.data?.visitType?.display ?? t('pastVisit', 'Past visit'), + }), + title: !visitToEdit + ? t('visitStarted', 'Visit started') + : t('visitDetailsUpdated', 'Visit details updated'), + }); + return response; }) .catch((error) => { showSnackbar({ @@ -608,6 +508,44 @@ const StartVisitForm: React.FC = ({ isLowContrast: false, subtitle: error?.message, }); + return Promise.reject(error); // short-circuit promise chain + }) + .then((response) => { + // now that visit is created / updated, we run post-submit actions + // to update visit attributes or any other OnVisitCreatedOrUpdated actions + const visit = response.data; + + // handleVisitAttributes already has code to show error snackbar when attribute fails to update + // no need for catch block here + const visitAttributesRequest = handleVisitAttributes(visitAttributes, response.data.uuid).then( + (visitAttributesResponses) => { + if (visitAttributesResponses.length > 0) { + showSnackbar({ + isLowContrast: true, + kind: 'success', + title: t('visitAttributesUpdatedSuccessfully', 'Visit attributes updated successfully'), + }); + } + }, + ); + + const OnVisitCreatedOrUpdatedRequests = [...OnVisitCreatedOrUpdatedCallbacks.values()].map( + (OnVisitCreatedOrUpdated) => OnVisitCreatedOrUpdated(visit, patientUuid), + ); + + return Promise.all([visitAttributesRequest, ...OnVisitCreatedOrUpdatedRequests]); + }) + .then(() => { + closeWorkspace({ ignoreChanges: true }); + }) + .catch(() => { + // do nothing, this catches any reject promises used for short-circuiting + }) + .finally(() => { + setIsSubmitting(false); + mutateCurrentVisit(); + mutateVisits(); + mutateInfiniteVisits(); }); } else { createOfflineVisitForPatient( @@ -615,28 +553,33 @@ const StartVisitForm: React.FC = ({ visitLocation.uuid, config.offlineVisitTypeUuid, payload.startDatetime, - ).then( - () => { - mutate(); - closeWorkspace({ ignoreChanges: true }); - showSnackbar({ - isLowContrast: true, - kind: 'success', - subtitle: t('visitStartedSuccessfully', '{{visit}} started successfully', { - visit: t('offlineVisit', 'Offline Visit'), - }), - title: t('visitStarted', 'Visit started'), - }); - }, - (error: Error) => { - showSnackbar({ - title: t('startVisitError', 'Error starting visit'), - kind: 'error', - isLowContrast: false, - subtitle: error?.message, - }); - }, - ); + ) + .then( + () => { + mutateCurrentVisit(); + closeWorkspace({ ignoreChanges: true }); + showSnackbar({ + isLowContrast: true, + kind: 'success', + subtitle: t('visitStartedSuccessfully', '{{visit}} started successfully', { + visit: t('offlineVisit', 'Offline Visit'), + }), + title: t('visitStarted', 'Visit started'), + }); + }, + (error: Error) => { + showSnackbar({ + title: t('startVisitError', 'Error starting visit'), + kind: 'error', + isLowContrast: false, + subtitle: error?.message, + }); + }, + ) + .finally(() => { + setIsSubmitting(false); + }); + return; } }, @@ -644,28 +587,17 @@ const StartVisitForm: React.FC = ({ closeWorkspace, config.offlineVisitTypeUuid, config.showExtraVisitAttributesSlot, - config.showServiceQueueFields, - config.showUpcomingAppointments, displayVisitStopDateTimeFields, extraVisitInfo, handleVisitAttributes, isOnline, - mutate, - mutateAppointments, mutateCurrentVisit, - mutateQueueEntry, mutateVisits, mutateInfiniteVisits, + OnVisitCreatedOrUpdatedCallbacks, patientUuid, - priority, - queueLocation, - service, - sortWeight, - status, t, - upcomingAppointment, validateVisitStartStopDatetime, - visitQueueNumberAttributeUuid, visitToEdit, ], ); @@ -726,15 +658,18 @@ const StartVisitForm: React.FC = ({ /> )} - {/* Upcoming appointments. This get shown when upcoming appointments are configured */} - {config.showUpcomingAppointments && ( -
-
-
- -
-
- )} + {/* Upcoming appointments. This get shown when config.showUpcomingAppointments is true. */} +
+
+
+ +
+
{/* This field lets the user select a location for the visit. The location is required for the visit to be saved. Defaults to the active session location */} @@ -833,15 +768,19 @@ const StartVisitForm: React.FC = ({ - {/* Queue location and queue fields. These get shown when queue location and queue fields are configured */} - {config.showServiceQueueFields && ( -
-
-
- -
-
- )} + {/* Queue location and queue fields. These get shown when config.showServiceQueueFields is true, + or when the form is opened from the queues app */} +
+
+
+ +
+
= ({ ); }; -function useConditionalVisitTypes() { - const isOnline = useConnectivity(); +interface VisitFormExtensionSlotProps { + name: string; + patientUuid: string; + visitFormOpenedFrom: string; + setOnVisitCreatedOrUpdatedCallbacks: React.Dispatch< + React.SetStateAction> + >; +} - const visitTypesHook = isOnline ? useVisitTypes : useOfflineVisitType; +type VisitFormExtensionState = { + patientUuid: string; - return visitTypesHook(); -} + /** + * This function allows an extension to register a callback to run after a visit has been created. + * This callback can be used to make further requests. The callback should handle its own UI notification + * on success / failure, and its returned Promise MUST resolve on success and MUST reject on failure. + * @param callback + * @returns + */ + setOnVisitCreatedOrUpdated: (callback: OnVisitCreatedOrUpdatedCallback) => void; + + visitFormOpenedFrom: string; + patientChartConfig: ChartConfig; +}; + +const VisitFormExtensionSlot: React.FC = ({ + name, + patientUuid, + visitFormOpenedFrom, + setOnVisitCreatedOrUpdatedCallbacks, +}) => { + const config = useConfig(); + + return ( + + {(extension) => { + const state: VisitFormExtensionState = { + patientUuid, + setOnVisitCreatedOrUpdated: (callback) => { + setOnVisitCreatedOrUpdatedCallbacks((old) => { + return new Map(old).set(extension.id, callback); + }); + }, + visitFormOpenedFrom, + patientChartConfig: config, + }; + return ; + }} + + ); +}; export default StartVisitForm; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.resource.ts b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.resource.ts index d36dff2415..bb22db8f14 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.resource.ts +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.resource.ts @@ -1,4 +1,7 @@ +import { openmrsFetch, restBaseUrl, useConnectivity, useVisitTypes, type Visit } from '@openmrs/esm-framework'; import { type amPm } from '@openmrs/esm-patient-common-lib'; +import { useOfflineVisitType } from '../hooks/useOfflineVisitType'; +import { useState } from 'react'; export type VisitFormData = { visitStartDate: Date; @@ -17,3 +20,39 @@ export type VisitFormData = { [x: string]: string; }; }; + +export function useConditionalVisitTypes() { + const isOnline = useConnectivity(); + + const visitTypesHook = isOnline ? useVisitTypes : useOfflineVisitType; + + return visitTypesHook(); +} + +export type OnVisitCreatedOrUpdatedCallback = (visit: Visit, patientUuid: string) => Promise; + +export function useOnVisitCreatedOrUpdatedCallbacks() { + return useState>(new Map()); +} + +export function createVisitAttribute(visitUuid: string, attributeType: string, value: string) { + return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute`, { + method: 'POST', + headers: { 'Content-type': 'application/json' }, + body: { attributeType, value }, + }); +} + +export function updateVisitAttribute(visitUuid: string, visitAttributeUuid: string, value: string) { + return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${visitAttributeUuid}`, { + method: 'POST', + headers: { 'Content-type': 'application/json' }, + body: { value }, + }); +} + +export function deleteVisitAttribute(visitUuid: string, visitAttributeUuid: string) { + return openmrsFetch(`${restBaseUrl}/visit/${visitUuid}/attribute/${visitAttributeUuid}`, { + method: 'DELETE', + }); +} diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx index 376f955b9d..6481397097 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.test.tsx @@ -1,12 +1,5 @@ -import React from 'react'; -import dayjs from 'dayjs'; -import { render, screen } from '@testing-library/react'; -import { esmPatientChartSchema, type ChartConfig } from '../../config-schema'; -import userEvent from '@testing-library/user-event'; import { getDefaultsFromConfigSchema, - openmrsFetch, - restBaseUrl, saveVisit, showSnackbar, updateVisit, @@ -17,11 +10,22 @@ import { type FetchResponse, type Visit, } from '@openmrs/esm-framework'; -import { mockPatient } from 'tools'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { mockLocations, mockVisitTypes, mockVisitWithAttributes } from '__mocks__'; +import dayjs from 'dayjs'; +import React from 'react'; +import { mockPatient } from 'tools'; +import { esmPatientChartSchema, type ChartConfig } from '../../config-schema'; import { useEmrConfiguration } from '../hooks/useEmrConfiguration'; import { useVisitAttributeType } from '../hooks/useVisitAttributeType'; import StartVisitForm from './visit-form.component'; +import { + createVisitAttribute, + deleteVisitAttribute, + updateVisitAttribute, + useOnVisitCreatedOrUpdatedCallbacks, +} from './visit-form.resource'; const visitUuid = 'test_visit_uuid'; const visitAttributes = { @@ -52,6 +56,7 @@ const mockPromptBeforeClosing = jest.fn(); const mockSetTitle = jest.fn(); const testProps = { + openedFrom: 'test', patientUuid: mockPatient.id, closeWorkspace: mockCloseWorkspace, closeWorkspaceWithSavedChanges: mockCloseWorkspace, @@ -62,7 +67,6 @@ const testProps = { const mockSaveVisit = jest.mocked(saveVisit); const mockUpdateVisit = jest.mocked(updateVisit); -const mockOpenmrsFetch = jest.mocked(openmrsFetch); const mockUseConfig = jest.mocked(useConfig); const mockUseVisitAttributeType = jest.mocked(useVisitAttributeType); const mockUseVisitTypes = jest.mocked(useVisitTypes); @@ -70,6 +74,16 @@ const mockUsePatient = jest.mocked(usePatient); const mockUseLocations = jest.mocked(useLocations); const mockUseEmrConfiguration = jest.mocked(useEmrConfiguration); +// from ./visit-form.resource +const mockOnVisitCreatedOrUpdatedCallback = jest.fn(); +jest.mocked(useOnVisitCreatedOrUpdatedCallbacks).mockReturnValue([ + new Map([['test-extension-id', mockOnVisitCreatedOrUpdatedCallback]]), // OnVisitCreatedOrUpdatedCallbacks + jest.fn(), // setOnVisitCreatedOrUpdatedCallbacks +]); +const mockCreateVisitAttribute = jest.mocked(createVisitAttribute).mockResolvedValue({} as unknown as FetchResponse); +const mockUpdateVisitAttribute = jest.mocked(updateVisitAttribute).mockResolvedValue({} as unknown as FetchResponse); +const mockDeleteVisitAttribute = jest.mocked(deleteVisitAttribute).mockResolvedValue({} as unknown as FetchResponse); + jest.mock('@openmrs/esm-patient-common-lib', () => ({ ...jest.requireActual('@openmrs/esm-patient-common-lib'), useActivePatientEnrollment: jest.fn().mockReturnValue({ @@ -98,7 +112,7 @@ jest.mock('../hooks/useVisitAttributeType', () => ({ useVisitAttributeTypes: jest.fn(() => ({ isLoading: false, error: null, - data: [visitAttributes.punctuality, visitAttributes.insurancePolicyNumber], + visitAttributeTypes: [visitAttributes.punctuality, visitAttributes.insurancePolicyNumber], })), useConceptAnswersForVisitAttributeType: jest.fn(() => ({ isLoading: false, @@ -146,6 +160,27 @@ jest.mock('../hooks/useDefaultFacilityLocation', () => { }; }); +jest.mock('./visit-form.resource', () => { + const requireActual = jest.requireActual('./visit-form.resource'); + return { + ...requireActual, + useOnVisitCreatedOrUpdatedCallbacks: jest.fn(), + createVisitAttribute: jest.fn(), + updateVisitAttribute: jest.fn(), + deleteVisitAttribute: jest.fn(), + }; +}); + +mockSaveVisit.mockResolvedValue({ + status: 201, + data: { + uuid: visitUuid, + visitType: { + display: 'Facility Visit', + }, + }, +} as unknown as FetchResponse); + describe('Visit form', () => { beforeEach(() => { mockUseConfig.mockReturnValue({ @@ -275,15 +310,6 @@ describe('Visit form', () => { it('starts a new visit upon successful submission of the form', async () => { const user = userEvent.setup(); - mockSaveVisit.mockResolvedValue({ - status: 201, - data: { - visitType: { - display: 'Facility Visit', - }, - }, - } as unknown as FetchResponse); - renderVisitForm(); const saveButton = screen.getByRole('button', { name: /Start visit/i }); @@ -320,8 +346,6 @@ describe('Visit form', () => { it('starts a new visit with attributes upon successful submission of the form', async () => { const user = userEvent.setup(); - mockOpenmrsFetch.mockResolvedValue({} as unknown as FetchResponse); - renderVisitForm(); const saveButton = screen.getByRole('button', { name: /Start visit/i }); @@ -341,16 +365,6 @@ describe('Visit form', () => { await user.clear(insuranceNumberInput); await user.type(insuranceNumberInput, '183299'); - mockSaveVisit.mockResolvedValue({ - status: 201, - data: { - uuid: visitUuid, - visitType: { - display: 'Facility Visit', - }, - }, - } as unknown as FetchResponse); - await user.click(saveButton); expect(mockSaveVisit).toHaveBeenCalledTimes(1); @@ -363,33 +377,39 @@ describe('Visit form', () => { expect.any(Object), ); - expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/visit/${visitUuid}/attribute`, { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { attributeType: visitAttributes.punctuality.uuid, value: '66cdc0a1-aa19-4676-af51-80f66d78d9eb' }, - }); + expect(mockCreateVisitAttribute).toHaveBeenCalledTimes(2); + expect(mockCreateVisitAttribute).toHaveBeenCalledWith( + visitUuid, + visitAttributes.punctuality.uuid, + '66cdc0a1-aa19-4676-af51-80f66d78d9eb', + ); + expect(mockCreateVisitAttribute).toHaveBeenCalledWith( + visitUuid, + visitAttributes.insurancePolicyNumber.uuid, + '183299', + ); - expect(mockOpenmrsFetch).toHaveBeenCalledWith(`${restBaseUrl}/visit/${visitUuid}/attribute`, { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { attributeType: visitAttributes.insurancePolicyNumber.uuid, value: '183299' }, - }); + expect(mockOnVisitCreatedOrUpdatedCallback).toHaveBeenCalled(); expect(mockCloseWorkspace).toHaveBeenCalled(); + expect(showSnackbar).toHaveBeenCalledTimes(2); expect(showSnackbar).toHaveBeenCalledWith({ isLowContrast: true, subtitle: expect.stringContaining('started successfully'), kind: 'success', title: 'Visit started', }); + expect(showSnackbar).toHaveBeenCalledWith({ + isLowContrast: true, + title: expect.stringContaining('Visit attributes updated successfully'), + kind: 'success', + }); }); it('updates visit attributes when editing an existing visit', async () => { const user = userEvent.setup(); - mockOpenmrsFetch.mockResolvedValue({} as unknown as FetchResponse); - renderVisitForm(mockVisitWithAttributes); const saveButton = screen.getByRole('button', { name: /Update visit/i }); @@ -430,23 +450,13 @@ describe('Visit form', () => { expect.any(Object), ); - expect(mockOpenmrsFetch).toHaveBeenCalledWith( - `${restBaseUrl}/visit/${visitUuid}/attribute/c98e66d7-7db5-47ae-b46f-91a0f3b6dda1`, - { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { value: '66cdc0a1-aa19-4676-af51-80f66d78d9ec' }, - }, - ); - - expect(mockOpenmrsFetch).toHaveBeenCalledWith( - `${restBaseUrl}/visit/${visitUuid}/attribute/d6d7d26a-5975-4f03-8abb-db073c948897`, - { - method: 'POST', - headers: { 'Content-type': 'application/json' }, - body: { value: '1873290' }, - }, + expect(mockUpdateVisitAttribute).toHaveBeenCalledTimes(2); + expect(mockUpdateVisitAttribute).toHaveBeenCalledWith( + visitUuid, + 'c98e66d7-7db5-47ae-b46f-91a0f3b6dda1', + '66cdc0a1-aa19-4676-af51-80f66d78d9ec', ); + expect(mockUpdateVisitAttribute).toHaveBeenCalledWith(visitUuid, 'd6d7d26a-5975-4f03-8abb-db073c948897', '1873290'); expect(mockCloseWorkspace).toHaveBeenCalled(); expect(showSnackbar).toHaveBeenCalledWith({ @@ -460,8 +470,6 @@ describe('Visit form', () => { it('deletes visit attributes if the value of the field is cleared when editing an existing visit', async () => { const user = userEvent.setup(); - mockOpenmrsFetch.mockResolvedValue({} as FetchResponse); - renderVisitForm(mockVisitWithAttributes); const saveButton = screen.getByRole('button', { name: /Update visit/i }); @@ -501,15 +509,9 @@ describe('Visit form', () => { expect.any(Object), ); - expect(mockOpenmrsFetch).toHaveBeenCalledWith( - `${restBaseUrl}/visit/${visitUuid}/attribute/c98e66d7-7db5-47ae-b46f-91a0f3b6dda1`, - { method: 'DELETE' }, - ); - - expect(mockOpenmrsFetch).toHaveBeenCalledWith( - `${restBaseUrl}/visit/${visitUuid}/attribute/d6d7d26a-5975-4f03-8abb-db073c948897`, - { method: 'DELETE' }, - ); + expect(mockDeleteVisitAttribute).toHaveBeenCalledTimes(2); + expect(mockDeleteVisitAttribute).toHaveBeenCalledWith(visitUuid, 'c98e66d7-7db5-47ae-b46f-91a0f3b6dda1'); + expect(mockDeleteVisitAttribute).toHaveBeenCalledWith(visitUuid, 'd6d7d26a-5975-4f03-8abb-db073c948897'); expect(mockCloseWorkspace).toHaveBeenCalled(); @@ -524,7 +526,7 @@ describe('Visit form', () => { it('renders an error message if there was a problem starting a new visit', async () => { const user = userEvent.setup(); - mockSaveVisit.mockRejectedValue({ status: 500, statusText: 'Internal server error' }); + mockSaveVisit.mockRejectedValueOnce({ status: 500, statusText: 'Internal server error' }); renderVisitForm(); @@ -533,7 +535,7 @@ describe('Visit form', () => { const saveButton = screen.getByRole('button', { name: /Start Visit/i }); const locationPicker = screen.getByRole('combobox', { name: /Select a location/i }); await user.click(locationPicker); - await user.click(screen.getByText('Inpatient Ward')); + await user.click(screen.getByText(/Inpatient Ward/i)); await user.click(saveButton); @@ -544,6 +546,56 @@ describe('Visit form', () => { title: 'Error starting visit', }), ); + + expect(mockOnVisitCreatedOrUpdatedCallback).not.toHaveBeenCalled(); + expect(mockCloseWorkspace).not.toHaveBeenCalled(); + }); + + it('renders an error message if there was a problem updating visit attributes after starting a new visit', async () => { + const user = userEvent.setup(); + + mockCreateVisitAttribute.mockRejectedValue({ status: 500, statusText: 'Internal server error' }); + + renderVisitForm(); + + await user.click(screen.getByLabelText(/Outpatient visit/i)); + + const saveButton = screen.getByRole('button', { name: /Start Visit/i }); + const locationPicker = screen.getByRole('combobox', { name: /Select a location/i }); + await user.click(locationPicker); + await user.click(screen.getByText(/Inpatient Ward/i)); + + const punctualityPicker = screen.getByRole('combobox', { name: 'Punctuality (optional)' }); + await user.selectOptions(punctualityPicker, 'On time'); + + const insuranceNumberInput = screen.getByRole('textbox', { name: 'Insurance Policy Number (optional)' }); + await user.clear(insuranceNumberInput); + await user.type(insuranceNumberInput, '183299'); + + await user.click(saveButton); + + expect(showSnackbar).toHaveBeenCalledTimes(3); + expect(showSnackbar).toHaveBeenCalledWith({ + isLowContrast: true, + subtitle: expect.stringContaining('started successfully'), + kind: 'success', + title: 'Visit started', + }); + expect(showSnackbar).toHaveBeenCalledWith({ + isLowContrast: false, + subtitle: undefined, + kind: 'error', + title: 'Error creating the Punctuality visit attribute', + }); + expect(showSnackbar).toHaveBeenCalledWith({ + isLowContrast: false, + subtitle: undefined, + kind: 'error', + title: 'Error creating the Insurance Policy Number visit attribute', + }); + + expect(mockOnVisitCreatedOrUpdatedCallback).toHaveBeenCalled(); + expect(mockCloseWorkspace).not.toHaveBeenCalled(); }); it('displays a warning modal if the user attempts to discard the visit form with unsaved changes', async () => { @@ -588,15 +640,6 @@ describe('Visit form', () => { ], }); - mockSaveVisit.mockResolvedValue({ - status: 201, - data: { - visitType: { - display: 'Facility Visit', - }, - }, - } as unknown as FetchResponse); - renderVisitForm(); const saveButton = screen.getByRole('button', { name: /Start visit/i }); diff --git a/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.component.tsx index 9ecfb9ea92..561be2cbe0 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.component.tsx @@ -38,9 +38,10 @@ const StartVisitDialog: React.FC = ({ launchPatientChartWithWorkspaceOpen({ patientUuid, workspaceName: 'start-visit-workspace-form', + additionalProps: {openedFrom: 'patient-chart-start-visit'} }); } else { - launchPatientWorkspace('start-visit-workspace-form'); + launchPatientWorkspace('start-visit-workspace-form', {openedFrom: 'patient-chart-start-visit'}); } closeModal(); diff --git a/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.test.tsx index d9a9475fcb..567c7294f7 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-prompt/start-visit-dialog.test.tsx @@ -35,7 +35,9 @@ describe('StartVisit', () => { await user.click(startNewVisitButton); - expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form'); + expect(launchPatientWorkspace).toHaveBeenCalledWith('start-visit-workspace-form', { + openedFrom: 'patient-chart-start-visit', + }); }); test('should launch edit past visit form', async () => { diff --git a/packages/esm-patient-chart-app/src/visit/visits-widget/current-visit-summary.component.tsx b/packages/esm-patient-chart-app/src/visit/visits-widget/current-visit-summary.component.tsx index 782594c4e1..aa473fd442 100644 --- a/packages/esm-patient-chart-app/src/visit/visits-widget/current-visit-summary.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visits-widget/current-visit-summary.component.tsx @@ -33,7 +33,7 @@ const CurrentVisitSummary: React.FC = ({ patientUuid } launchPatientWorkspace('start-visit-workspace-form')} + launchForm={() => launchPatientWorkspace('start-visit-workspace-form', {openedFrom: 'patient-chart-current-visit-summary'})} /> ); } diff --git a/packages/esm-patient-chart-app/translations/en.json b/packages/esm-patient-chart-app/translations/en.json index bef2c0bf30..e20a0c7680 100644 --- a/packages/esm-patient-chart-app/translations/en.json +++ b/packages/esm-patient-chart-app/translations/en.json @@ -4,8 +4,6 @@ "addPastVisitText": "You can add a new past visit or update an old one. Choose from one of the options below to continue.", "all": "All", "allEncounters": "All encounters", - "appointmentCheckedIn": "Appointment checked in", - "appointmentMarkedChecked": "Appointment marked as Checked In", "cancel": "Cancel", "cancelActiveVisitConfirmation": "Are you sure you want to cancel this active visit?", "cancellingVisit": "Cancelling visit", @@ -122,8 +120,6 @@ "program": "Program", "provider": "Provider", "quantity": "Quantity", - "queueAddedSuccessfully": "Patient has been added to the queue successfully.", - "queueEntryError": "Error adding patient to the queue", "recommended": "Recommended", "record": "Record", "refills": "Refills", @@ -153,11 +149,11 @@ "timeFormat ": "Time Format", "type": "Type", "undo": "Undo", - "updateError": "Error updating upcoming appointment", "updateVisit": "Update visit", "updatingVisit": "Updating visit", "visit": "Visit", "visitAttributes": "Visit attributes", + "visitAttributesUpdatedSuccessfully": "Visit attributes updated successfully", "visitCancelled": "Visit cancelled", "visitCancelSuccessMessage": "Active {{visit}} cancelled successfully", "visitDeleted": "{{visit}} deleted",