import React, { useMemo, useState } from 'react'; import classNames from 'classnames'; import dayjs from 'dayjs'; import { Button, Checkbox, ComboBox, InlineNotification, ModalBody, ModalFooter, ModalHeader, RadioButton, RadioButtonGroup, SelectItem, Stack, TextArea, TimePicker, TimePickerSelect, } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import { getCoreTranslation, OpenmrsDatePicker, showSnackbar, type FetchResponse, useConfig, } from '@openmrs/esm-framework'; import { useMutateQueueEntries } from '../hooks/useQueueEntries'; import { useQueues } from '../hooks/useQueues'; import { time12HourFormatRegexPattern } from '../constants'; import { getErrorMessage, isAlreadyEndedQueueEntryError, isDuplicateQueueEntryError } from './queue-entry-error.utils'; import { convertTime12to24, type amPm } from './time-helpers'; import { type ConfigObject } from '../config-schema'; import { type Queue, type QueueEntry } from '../types'; import QueuePriority from '../queue-table/components/queue-priority.component'; import styles from './queue-entry-actions.scss'; const TIME_REGEX_12H = /^(0?[1-9]|1[0-2]):[0-5][0-9]$/; const TIME_REGEX_24H = /^(1[3-9]|2[0-3]):[0-5][0-9]$/; const TIME_REGEX_ZERO_HOUR = /^00:[0-5][0-9]$/; const getTimeFormatError = (timeStr: string, t: (key: string, defaultValue: string) => string) => { if (TIME_REGEX_24H.test(timeStr)) { return t('use12HourFormat', 'Please use 12-hour format with AM/PM (01–12)'); } if (TIME_REGEX_ZERO_HOUR.test(timeStr)) { return t('useMidnightFormat', 'For midnight use 12:00 AM, for noon use 12:00 PM'); } return t('invalidTimeFormat', 'Invalid time format'); }; interface QueueEntryActionModalProps { queueEntry: QueueEntry; closeModal: () => void; modalParams: ModalParams; } interface FormState { selectedQueue: string; selectedPriority: string; selectedStatus: string; priorityComment: string; modifyDefaultTransitionDateTime: boolean; transitionDate: Date; transitionTime: string; transitionTimeFormat: amPm; } interface ModalParams { modalTitle: string; modalInstruction?: string; submitButtonText: string; submitSuccessTitle: string; submitSuccessText: string; submitFailureTitle: string; submitAction: (queueEntry: QueueEntry, formState: FormState) => Promise>; disableSubmit: (queueEntry: QueueEntry, formState: FormState) => boolean; isEdit: boolean; // editing existing queue entry? showQueuePicker: boolean; showStatusPicker: boolean; } // This file has suffix .component.tsx and not .modal.tsx because it is not itself a modal; // rather it is a shared component that implements the UIs of a few different modals, // and is used by them. /** * Modal for performing a queue entry action that requires additional form fields / inputs from user * Used by EditQueueEntryModal, MoveQueueEntryModal, and TransitionQueueEntryModal */ export const QueueEntryActionModal: React.FC = ({ queueEntry, closeModal, modalParams, }) => { const config = useConfig(); const { t } = useTranslation(); const { mutateQueueEntries } = useMutateQueueEntries(); const { modalTitle, modalInstruction, submitButtonText, submitSuccessTitle, submitSuccessText, submitFailureTitle, submitAction, disableSubmit, isEdit, showQueuePicker, showStatusPicker, } = modalParams; const initialTransitionDate = isEdit ? new Date(queueEntry.startedAt) : new Date(); const [formState, setFormState] = useState({ selectedQueue: queueEntry.queue.uuid, selectedPriority: queueEntry.priority.uuid, selectedStatus: queueEntry.status.uuid, priorityComment: queueEntry.priorityComment ?? '', modifyDefaultTransitionDateTime: false, transitionDate: initialTransitionDate, transitionTime: dayjs(initialTransitionDate).format('hh:mm'), transitionTimeFormat: dayjs(initialTransitionDate).hour() < 12 ? 'AM' : 'PM', }); const { queues } = useQueues(); const [isSubmitting, setIsSubmitting] = useState(false); const [submissionError, setSubmissionError] = useState<{ type: 'duplicate' | 'error'; message: string; title?: string; } | null>(null); const selectedQueue = queues.find((q) => q.uuid === formState.selectedQueue); const clearSubmissionError = () => setSubmissionError(null); const statuses = selectedQueue?.allowedStatuses; const hasNoStatusesConfigured = selectedQueue && statuses.length === 0; const priorities = selectedQueue?.allowedPriorities; const hasNoPrioritiesConfigured = selectedQueue && priorities.length === 0; const setSelectedQueueUuid = (selectedQueueUuid: string) => { clearSubmissionError(); const newSelectedQueue = queues.find((queue) => queue.uuid === selectedQueueUuid); if (!newSelectedQueue) { return; } const { allowedStatuses, allowedPriorities } = newSelectedQueue; const newQueueHasCurrentPriority = allowedPriorities.find((s) => s.uuid === formState.selectedPriority); const defaultStatusUuid = config.concepts.defaultStatusConceptUuid; const newQueueHasDefaultStatus = allowedStatuses.find((s) => s.uuid === defaultStatusUuid); const newStatus = newQueueHasDefaultStatus ? defaultStatusUuid : allowedStatuses[0]?.uuid; setFormState((prev) => ({ ...prev, selectedQueue: selectedQueueUuid, selectedStatus: newStatus, selectedPriority: newQueueHasCurrentPriority ? prev.selectedPriority : allowedPriorities[0]?.uuid, })); }; const setSelectedPriorityUuid = (selectedPriorityUuid: string) => { clearSubmissionError(); setFormState((prev) => ({ ...prev, selectedPriority: selectedPriorityUuid })); }; const setSelectedStatusUuid = (selectedStatusUuid: string) => { clearSubmissionError(); setFormState((prev) => ({ ...prev, selectedStatus: selectedStatusUuid })); }; const setPriorityComment = (priorityComment: string) => { setFormState((prev) => ({ ...prev, priorityComment })); }; const setTransitionDate = (transitionDate: Date) => { setFormState((prev) => ({ ...prev, transitionDate })); }; const setTransitionTime = (transitionTime: string) => { setFormState((prev) => ({ ...prev, transitionTime })); }; const setTransitionTimeFormat = (transitionTimeFormat: amPm) => { setFormState((prev) => ({ ...prev, transitionTimeFormat })); }; const setModifyDefaultTransitionDateTime = (modifyDefaultTransitionDateTime: boolean) => { setFormState((prev) => ({ ...prev, modifyDefaultTransitionDateTime })); }; const submitForm = () => { setIsSubmitting(true); submitAction(queueEntry, formState) .then(({ status }) => { if (status === 200) { showSnackbar({ isLowContrast: true, title: submitSuccessTitle, kind: 'success', subtitle: submitSuccessText, }); mutateQueueEntries(); closeModal(); } else { throw { message: t('unexpectedServerResponse', 'Unexpected Server Response') }; } }) .catch((error) => { if (isAlreadyEndedQueueEntryError(error)) { showSnackbar({ title: t('queueEntryAlreadyEnded', 'Queue entry is no longer active'), kind: 'warning', subtitle: t( 'queueEntryAlreadyEndedMessage', 'This queue entry has already been completed by another user. The queue has been refreshed.', ), }); mutateQueueEntries(); closeModal(); } else if (isDuplicateQueueEntryError(error)) { setSubmissionError({ type: 'duplicate', message: t('duplicateQueueEntry', 'This patient is already in the selected queue.'), title: t('patientAlreadyInQueue', 'Patient already in queue'), }); } else { setSubmissionError({ type: 'error', message: getErrorMessage(error) || t('unknownError', 'An unknown error occurred'), title: submitFailureTitle, }); } }) .finally(() => { setIsSubmitting(false); }); }; // non-null if the selected date+time is invalid const timeInvalidMessage = useMemo(() => { if (!TIME_REGEX_12H.test(formState.transitionTime)) { return getTimeFormatError(formState.transitionTime, t); } const now = new Date(); const startAtDate = new Date(formState.transitionDate); const [hour, minute] = convertTime12to24(formState.transitionTime, formState.transitionTimeFormat); startAtDate.setHours(hour, minute, 0, 0); const previousQueueEntryStartTimeStr = queueEntry.previousQueueEntry?.startedAt; const previousQueueEntryStartTime = previousQueueEntryStartTimeStr ? new Date(previousQueueEntryStartTimeStr) : null; if (startAtDate > now) { return t('timeCannotBeInFuture', 'Time cannot be in the future'); } if (previousQueueEntryStartTime) { const previousMinutePrecision = new Date(previousQueueEntryStartTime); previousMinutePrecision.setSeconds(0, 0); if (startAtDate < previousMinutePrecision) { return t( 'timeCannotBePriorToPreviousQueueEntry', 'Time cannot be before start of previous queue entry: {{time}}', { time: previousMinutePrecision.toLocaleString(), interpolation: { escapeValue: false }, }, ); } } return null; }, [ formState.transitionDate, formState.transitionTime, formState.transitionTimeFormat, queueEntry.previousQueueEntry?.startedAt, t, ]); const getQueueDisplayText = (queue: Queue) => { if (!queue) return ''; return queue.uuid === queueEntry.queue.uuid ? t('currentValueFormatted', '{{value}} (Current)', { value: `${queue.display} - ${queue.location?.display}` }) : `${queue.display} - ${queue.location?.display}`; }; return ( <>
{modalInstruction &&

{modalInstruction}

} {showQueuePicker && (
{/* Read this issue description for why we're using 8 locations as the cut off https://openmrs.atlassian.net/jira/software/c/projects/O3/issues/O3-4131 */} {queues.length <= 8 ? ( setSelectedQueueUuid(String(uuid))}> {queues?.map((queue) => { const { uuid } = queue; return ; })} ) : ( id="queue" titleText={t('serviceLocation', 'Service location')} selectedItem={selectedQueue} items={queues} itemToString={getQueueDisplayText} onChange={({ selectedItem }) => { if (selectedItem) { setSelectedQueueUuid(selectedItem.uuid); } }} shouldFilterItem={(menu) => { const itemDisplay = (menu?.item?.display ?? '').toLowerCase(); const locationDisplay = (menu?.item?.location?.display ?? '').toLowerCase(); const inputValue = (menu?.inputValue ?? '').toLowerCase(); return itemDisplay.includes(inputValue) || locationDisplay.includes(inputValue); }} /> )}
)} {showStatusPicker && (
{t('status', 'Status')}
{hasNoStatusesConfigured ? ( ) : ( { setSelectedStatusUuid(String(uuid)); }}> {statuses?.map(({ uuid, display }) => ( ))} )}
)}
{t('priority', 'Priority')}
{hasNoPrioritiesConfigured ? ( ) : ( { setSelectedPriorityUuid(String(uuid)); }}> {priorities?.map(({ uuid, display }) => ( } value={uuid} /> ))} )}
{t('comment', 'Comment')}