From 6cef430fc61f1e9647e435d32305c91bcf6b5cb4 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Mon, 18 Dec 2023 14:55:39 +0100 Subject: [PATCH] feat: [DHIS2-15212][DHIS2-15214][DHIS2-15216] Referral widget actions (#3458) --- i18n/en.pot | 30 +++- .../DataEntry/CancelButton.component.js | 2 + .../components/DataEntry/withCancelButton.js | 4 +- .../EnrollmentAddEventPage.epics.js | 77 +++++++++- ...EnrollmentAddEventPageDefault.container.js | 7 +- .../enrollment.actions.js | 4 + .../FinishButtons/FinishButtons.component.js | 18 ++- .../FinishButtons/finishButtons.types.js | 3 + .../Validated/Validated.component.js | 6 + .../Validated/Validated.container.js | 134 ++++++++---------- .../getConvertedReferralEvent.js | 91 ++++++++++-- .../getConvertedReferralEvent.types.js | 5 +- .../Validated/useBuildNewEventPayload.js | 129 +++++++++++++++++ .../Validated/validated.actions.js | 36 ++++- .../Validated/validated.epics.js | 56 ++------ .../Validated/validated.types.js | 32 +++-- .../WidgetEventSchedule.epics.js | 3 +- .../LinkToExisting.component.js | 74 ++++++++++ .../LinkToExisting/LinkToExisting.types.js | 14 ++ .../WidgetReferral/LinkToExisting/index.js | 2 + .../ReferralActions.component.js | 87 ++++++++++-- .../ReferralActions/ReferralActions.types.js | 24 ++-- .../WidgetReferral.component.js | 31 +++- .../WidgetReferral/WidgetReferral.types.js | 3 + .../hooks/useAvailableReferralEvents.js | 82 +++++++++++ .../WidgetReferral/hooks/useScheduledLabel.js | 27 ---- .../WidgetReferral/hooks/useStageLabels.js | 34 +++++ .../ValidationFunctions.js | 71 ++++++++++ .../referralEventIsValid.js | 48 +++---- .../referralEventIsValid.types.js | 3 + .../components/WidgetReferral/useReferral.js | 3 +- .../enrollmentDomain.reducerDescription.js | 24 +++- .../query/useApiDataQuery.js | 4 +- 33 files changed, 923 insertions(+), 245 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useBuildNewEventPayload.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.component.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.types.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/index.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/hooks/useAvailableReferralEvents.js delete mode 100644 src/core_modules/capture-core/components/WidgetReferral/hooks/useScheduledLabel.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/hooks/useStageLabels.js create mode 100644 src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/ValidationFunctions.js diff --git a/i18n/en.pot b/i18n/en.pot index e2838c1b69..dc4398bd87 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-10-30T08:21:47.926Z\n" -"PO-Revision-Date: 2023-10-30T08:21:47.926Z\n" +"POT-Creation-Date: 2023-11-04T16:21:37.396Z\n" +"PO-Revision-Date: 2023-11-04T16:21:37.396Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1329,12 +1329,32 @@ msgstr "Edit" msgid "tracked entity instance" msgstr "tracked entity instance" +msgid "Link to an existing {{referralProgramStageLabel}}" +msgstr "Link to an existing {{referralProgramStageLabel}}" + +msgid "Choose a {{referralProgramStageLabel}}" +msgstr "Choose a {{referralProgramStageLabel}}" + msgid "Referral actions" msgstr "Referral actions" msgid "Ambiguous referrals, contact system administrator" msgstr "Ambiguous referrals, contact system administrator" +msgid "" +"Enter {{referralProgramStageLabel}} details in the next step after " +"completing this {{currentStageLabel}}." +msgstr "" +"Enter {{referralProgramStageLabel}} details in the next step after " +"completing this {{currentStageLabel}}." + +msgid "" +"This {{currentStageLabel}} will be created without a link to " +"{{referralProgramStageLabel}}" +msgstr "" +"This {{currentStageLabel}} will be created without a link to " +"{{referralProgramStageLabel}}" + msgid "Schedule in {{displayName}}" msgstr "Schedule in {{displayName}}" @@ -1350,6 +1370,12 @@ msgstr "Don't link to a {{displayName}}" msgid "Scheduled date" msgstr "Scheduled date" +msgid "Report date" +msgstr "Report date" + +msgid "Please select a valid event" +msgstr "Please select a valid event" + msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" diff --git a/src/core_modules/capture-core/components/DataEntry/CancelButton.component.js b/src/core_modules/capture-core/components/DataEntry/CancelButton.component.js index ca41bcaec2..679e1ade59 100644 --- a/src/core_modules/capture-core/components/DataEntry/CancelButton.component.js +++ b/src/core_modules/capture-core/components/DataEntry/CancelButton.component.js @@ -7,6 +7,7 @@ import { defaultDialogProps } from '../Dialogs/DiscardDialog.constants'; type Props = { dataEntryHasChanges: boolean, + disabled: boolean, onCancel: () => void, } @@ -36,6 +37,7 @@ export class CancelButtonComponent extends React.Component {
-
diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js index dd3acb8e32..b87b575b36 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js @@ -5,11 +5,14 @@ import { typeof addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; export type InputProps = {| onSave: (saveType: $Keys) => void, onCancel: () => void, + isLoading: ?boolean, + cancelButtonIsDisabled?: boolean, id: string, |}; export type Props = {| onSave: (saveType: $Keys) => void, cancelButton: Element, + isLoading: ?boolean, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js index a659a69086..d8966ab8fd 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js @@ -20,6 +20,8 @@ const ValidatedPlain = ({ stage, programName, programId, + enrollmentId, + eventSaveInProgress, formFoundation, classes, referralRef, @@ -47,12 +49,16 @@ const ValidatedPlain = ({ /> diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js index 548dad67ff..8603765e47 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js @@ -1,17 +1,23 @@ // @flow -import React, { useCallback, useRef } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { batchActions } from 'redux-batched-actions'; import { withAskToCreateNew, withSaveHandler } from '../../DataEntry'; import { useLifecycle } from './useLifecycle'; import { useClientFormattedRulesExecutionDependencies } from './useClientFormattedRulesExecutionDependencies'; import { ValidatedComponent } from './Validated.component'; -import { requestSaveEvent, startCreateNewAfterCompleting } from './validated.actions'; -import type { ContainerProps } from './validated.types'; +import { + cleanUpEventSaveInProgress, + newEventBatchActionTypes, + requestSaveEvent, + setSaveEnrollmentEventInProgress, + startCreateNewAfterCompleting, +} from './validated.actions'; +import type { ContainerProps, ReferralRefPayload } from './validated.types'; import type { RenderFoundation } from '../../../metaData'; import { addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; import { useAvailableProgramStages } from '../../../hooks'; -import { generateUID } from '../../../utils/uid/generateUID'; -import { getConvertedReferralEvent } from './getConvertedReferralEvent'; +import { useBuildNewEventPayload } from './useBuildNewEventPayload'; const SaveHandlerHOC = withSaveHandler()(ValidatedComponent); const AskToCreateNewHandlerHOC = withAskToCreateNew()(SaveHandlerHOC); @@ -31,7 +37,20 @@ export const Validated = ({ }: ContainerProps) => { const dataEntryId = 'enrollmentEvent'; const itemId = 'newEvent'; - const referralRef = useRef(); + const referralRef = useRef(null); + const eventSaveInProgress = useSelector( + ({ enrollmentDomain }) => !!enrollmentDomain.eventSaveInProgress?.requestEventId, + ); + const { buildNewEventPayload } = useBuildNewEventPayload({ + dataEntryId, + itemId, + programId: program.id, + orgUnitId: orgUnit.id, + orgUnitName: orgUnit.name, + teiId, + enrollmentId, + formFoundation, + }); const rulesExecutionDependenciesClientFormatted = useClientFormattedRulesExecutionDependencies(rulesExecutionDependencies, program); @@ -47,7 +66,6 @@ export const Validated = ({ rulesExecutionDependenciesClientFormatted, }); - const availableProgramStages = useAvailableProgramStages(stage, teiId, enrollmentId, program.id); const dispatch = useDispatch(); @@ -57,72 +75,37 @@ export const Validated = ({ formFoundationArgument: RenderFoundation, saveType: ?$Values, ) => { - const clientRequestEvent = { - dataEntryItemId, - eventId: generateUID(), - dataEntryId: dataEntryIdArgument, - formFoundation: formFoundationArgument, - programId: program.id, - orgUnitId: orgUnit.id, - orgUnitName: orgUnit.name || '', - teiId, - enrollmentId, - onSaveExternal, - }; - - if ( - referralRef.current - && saveType === addEventSaveTypes.COMPLETE - && referralRef.current.eventHasReferralRelationship() - ) { - const isValid = referralRef.current.formIsValidOnSave(); + // window.scrollTo(0, 0); + const { + clientRequestEvent, + formHasError, + referralEvent, + relationship, + referralMode, + } = buildNewEventPayload(saveType, referralRef); - if (isValid) { - const { referralValues, referralType } = referralRef.current - .getReferralValues(clientRequestEvent.eventId); + if (formHasError) return; - const { referralEvent, relationship } = getConvertedReferralEvent({ - referralDataValues: referralValues, - currentEventId: clientRequestEvent.eventId, - referralType, - programId: program.id, - currentProgramStageId: stage.id, - teiId, - enrollmentId, - }); - - dispatch(requestSaveEvent({ - requestEvent: { - completed: true, - ...clientRequestEvent, - }, - referralEvent, - relationship, - onSaveSuccessActionType, - onSaveErrorActionType, - })); - } - return; - } - - window.scrollTo(0, 0); - const completed = saveType === addEventSaveTypes.COMPLETE; - dispatch(requestSaveEvent({ - requestEvent: { completed, ...clientRequestEvent }, - onSaveSuccessActionType, - onSaveErrorActionType, - })); - }, [ - program, - orgUnit, - teiId, - enrollmentId, - onSaveExternal, - dispatch, - onSaveSuccessActionType, - onSaveErrorActionType, - stage.id, - ]); + dispatch(batchActions([ + requestSaveEvent({ + requestEvent: clientRequestEvent, + referralEvent, + relationship, + referralMode, + onSaveExternal, + onSaveSuccessActionType, + onSaveErrorActionType, + }), + // stores meta in redux to be used when navigating after save + setSaveEnrollmentEventInProgress({ + requestEventId: clientRequestEvent?.event, + referralEventId: referralEvent?.event, + referralOrgUnitId: referralEvent?.orgUnit, + referralMode, + }), + ], newEventBatchActionTypes.REQUEST_SAVE_AND_SET_SUBMISSION_IN_PROGRESS), + ); + }, [buildNewEventPayload, onSaveExternal, dispatch, onSaveSuccessActionType, onSaveErrorActionType]); const handleCreateNew = useCallback((isCreateNew?: boolean) => { handleSave(itemId, dataEntryId, formFoundation, addEventSaveTypes.COMPLETE); @@ -132,15 +115,22 @@ export const Validated = ({ })); }, [handleSave, formFoundation, dispatch, enrollmentId, orgUnit.id, program.id, teiId, availableProgramStages]); + // Clean up data entry on unmount in case the user navigates away, stopping delayed navigation + useEffect(() => () => { + dispatch(cleanUpEventSaveInProgress()); + }, [dispatch]); + return ( { - const { scheduledAt: referralScheduledAt, orgUnit: referralOrgUnit } = referralDataValues; - - const requestEventIsFromConstraint = referralType.fromConstraint.programStage.id === currentProgramStageId; - - const referralEvent = { + clientRequestEvent, +}) => { + const baseEventDetails = { event: generateUID(), program: programId, programStage: requestEventIsFromConstraint ? @@ -23,22 +23,83 @@ export const getConvertedReferralEvent = ({ : referralType.fromConstraint.programStage.id, trackedEntity: teiId, enrollment: enrollmentId, - scheduledAt: referralScheduledAt, - orgUnit: referralOrgUnit?.id, dataValues: [], status: 'SCHEDULE', }; - const relationship = { + if (referralMode === ReferralModes.REFER_ORG) { + const { scheduledAt: referralScheduledAt, orgUnit: referralOrgUnit } = referralDataValues; + + return ({ + referralEvent: { + ...baseEventDetails, + scheduledAt: referralScheduledAt, + orgUnit: referralOrgUnit?.id, + }, + linkedEventId: baseEventDetails.event, + }); + } else if (referralMode === ReferralModes.ENTER_DATA) { + return ({ + referralEvent: { + ...baseEventDetails, + scheduledAt: clientRequestEvent.occurredAt, + orgUnit: clientRequestEvent.orgUnit, + }, + linkedEventId: baseEventDetails.event, + }); + } else if (referralMode === ReferralModes.LINK_EXISTING_RESPONSE) { + const { linkedEventId } = referralDataValues; + return { + referralEvent: null, + linkedEventId, + }; + } else if (referralMode === ReferralModes.DO_NOT_LINK_RESPONSE) { + return { + referralEvent: null, + linkedEventId: null, + }; + } + + log.error(errorCreator(`Referral mode ${referralMode} is not supported`)()); + return { + referralEvent: null, + linkedEventId: null, + }; +}; + +export const getConvertedReferralEvent = ({ + referralMode, + referralDataValues, + programId, + teiId, + currentProgramStageId, + clientRequestEvent, + enrollmentId, + referralType, +}: ConvertedReferralEventProps) => { + const requestEventIsFromConstraint = referralType.fromConstraint.programStage.id === currentProgramStageId; + + const { referralEvent, linkedEventId } = getEventDetailsByReferralMode({ + referralDataValues, + requestEventIsFromConstraint, + referralMode, + referralType, + programId, + teiId, + enrollmentId, + clientRequestEvent, + }); + + const relationship = linkedEventId && { relationshipType: referralType.id, from: { event: { - event: requestEventIsFromConstraint ? currentEventId : referralEvent.event, + event: requestEventIsFromConstraint ? clientRequestEvent.event : linkedEventId, }, }, to: { event: { - event: requestEventIsFromConstraint ? referralEvent.event : currentEventId, + event: requestEventIsFromConstraint ? linkedEventId : clientRequestEvent.event, }, }, }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedReferralEvent/getConvertedReferralEvent.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedReferralEvent/getConvertedReferralEvent.types.js index 2f8bb212b1..e96510381d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedReferralEvent/getConvertedReferralEvent.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedReferralEvent/getConvertedReferralEvent.types.js @@ -1,5 +1,7 @@ // @flow import type { ReferralDataValueStates } from '../../../WidgetReferral'; +import { actions as ReferralModes } from '../../../WidgetReferral/constants'; +import type { RequestEvent } from '../validated.types'; type ReferralType = {| id: string, @@ -16,11 +18,12 @@ type ReferralType = {| |} export type ConvertedReferralEventProps = {| + referralMode: $Keys, referralDataValues: ReferralDataValueStates, programId: string, teiId: string, currentProgramStageId: string, - currentEventId: string, enrollmentId: string, referralType: ReferralType, + clientRequestEvent: RequestEvent, |} diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useBuildNewEventPayload.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useBuildNewEventPayload.js new file mode 100644 index 0000000000..61dcd98dca --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useBuildNewEventPayload.js @@ -0,0 +1,129 @@ +// @flow +import { useSelector } from 'react-redux'; +import type { RenderFoundation } from '../../../metaData'; +import { getAddEventEnrollmentServerData } from './getConvertedAddEvent'; +import { convertDataEntryToClientValues } from '../../DataEntry/common/convertDataEntryToClientValues'; +import { generateUID } from '../../../utils/uid/generateUID'; +import { addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; +import { getConvertedReferralEvent } from './getConvertedReferralEvent'; +import type { ReferralRefPayload } from './validated.types'; + +type Props = { + dataEntryId: string, + itemId: string, + orgUnitId: string, + programId: string, + formFoundation: RenderFoundation, + enrollmentId: string, + orgUnitName: string, + teiId: string, +}; + +export const useBuildNewEventPayload = ({ + dataEntryId, + itemId, + orgUnitId, + programId, + teiId, + enrollmentId, + orgUnitName, + formFoundation, +}: Props) => { + const dataEntryKey = `${dataEntryId}-${itemId}`; + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + const dataEntryValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); + const dataEntryValuesMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); + const notes = useSelector(({ dataEntriesNotes }) => dataEntriesNotes[dataEntryKey]); + + const buildReferralEventPayload = (clientRequestEvent, saveType: ?$Values, referralRef) => { + if ( + referralRef.current + && saveType === addEventSaveTypes.COMPLETE + && referralRef.current.eventHasReferralRelationship() + ) { + const isValid = referralRef.current.formIsValidOnSave(); + if (!isValid || !referralRef.current?.getReferralValues) { + return { + formHasError: true, + referralEvent: null, + relationship: null, + referralMode: null, + }; + } + + const { referralType, referralValues, referralMode } = referralRef.current + .getReferralValues(clientRequestEvent.event); + + const { referralEvent, relationship } = getConvertedReferralEvent({ + referralMode, + referralDataValues: referralValues, + clientRequestEvent, + referralType, + programId, + currentProgramStageId: formFoundation.id, + teiId, + enrollmentId, + }); + + return { + formHasError: false, + referralEvent, + relationship, + referralMode, + }; + } + return { + formHasError: false, + referralEvent: null, + relationship: null, + referralMode: null, + }; + }; + + const buildNewEventPayload = ( + saveType: ?$Values, + referralRef: {| current: (ReferralRefPayload | null) |}, + ) => { + const requestEventId = generateUID(); + + const { formClientValues, dataEntryClientValues } = convertDataEntryToClientValues( + formFoundation, + formValues, + dataEntryValues, + dataEntryValuesMeta, + ); + const notesValues = notes ? notes.map(note => ({ value: note.value })) : []; + + const clientRequestEvent = getAddEventEnrollmentServerData({ + formFoundation, + formClientValues, + eventId: requestEventId, + mainDataClientValues: { ...dataEntryClientValues, notes: notesValues }, + programId, + orgUnitId, + enrollmentId, + teiId, + orgUnitName, + completed: saveType === addEventSaveTypes.COMPLETE, + }); + + const { + formHasError, + referralEvent, + relationship, + referralMode, + } = buildReferralEventPayload(clientRequestEvent, saveType, referralRef); + + return { + formHasError, + clientRequestEvent, + referralEvent, + relationship, + referralMode, + }; + }; + + return { + buildNewEventPayload, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js index 6c76d05c52..ab5d4384f5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.actions.js @@ -1,30 +1,40 @@ // @flow import { actionCreator } from '../../../actions/actions.utils'; import { effectMethods } from '../../../trackerOffline'; +import { actions as ReferralModes } from '../../WidgetReferral/constants'; import type { RequestEvent } from './validated.types'; +import type { ExternalSaveHandler } from '../common.types'; + +export const newEventBatchActionTypes = { + REQUEST_SAVE_AND_SET_SUBMISSION_IN_PROGRESS: 'NewEvent.RequestSaveAndSetSubmissionInProgress', +}; export const newEventWidgetActionTypes = { RULES_ON_UPDATE_EXECUTE: 'NewEvent.ExecuteRulesOnUpdate', EVENT_SAVE_REQUEST: 'NewEvent.RequestSaveEvent', - EVENT_SAVE_REQUEST_WITH_REFERRAL: 'NewEvent.RequestSaveEventWithReferral', - REQUEST_SAVE_REFERRAL_IF_EXISTS: 'NewEvent.RequestSaveReferralIfExists', EVENT_SAVE: 'NewEvent.SaveEvent', EVENT_SAVE_SUCCESS: 'NewEvent.SaveEventSuccess', // TEMPORARY - pass in success action name to the widget EVENT_SAVE_ERROR: 'NewEvent.SaveEventError', // TEMPORARY - pass in error action name to the widget EVENT_NOTE_ADD: 'NewEvent.AddEventNote', START_CREATE_NEW_AFTER_COMPLETING: 'NewEvent.StartCreateNewAfterCompleting', + SET_SAVE_ENROLLMENT_EVENT_IN_PROGRESS: 'NewEvent.SetSaveEnrollmentEventInProgress', + CLEAN_UP_EVENT_SAVE_IN_PROGRESS: 'NewEvent.CleanUpDataEntry', }; export const requestSaveEvent = ({ requestEvent, referralEvent, relationship, + referralMode, + onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, }: { requestEvent: RequestEvent, referralEvent?: Object, relationship?: Object, + referralMode: ?$Keys, + onSaveExternal: ?ExternalSaveHandler, onSaveSuccessActionType?: string, onSaveErrorActionType?: string, }) => @@ -32,10 +42,29 @@ export const requestSaveEvent = ({ requestEvent, referralEvent, relationship, + referralMode, + onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, }, { skipLogging: ['formFoundation'] }); +export const setSaveEnrollmentEventInProgress = ({ + requestEventId, + referralEventId, + referralOrgUnitId, + referralMode, +}: { + requestEventId: string, + referralEventId: ?string, + referralOrgUnitId: ?string, + referralMode: ?$Keys, +}) => actionCreator(newEventWidgetActionTypes.SET_SAVE_ENROLLMENT_EVENT_IN_PROGRESS)({ + requestEventId, + referralEventId, + referralOrgUnitId, + referralMode, +}); + export const saveEvents = ({ serverData, onSaveErrorActionType, onSaveSuccessActionType }: Object) => actionCreator(newEventWidgetActionTypes.EVENT_SAVE)({}, { offline: { @@ -51,3 +80,6 @@ export const saveEvents = ({ serverData, onSaveErrorActionType, onSaveSuccessAct export const startCreateNewAfterCompleting = ({ enrollmentId, isCreateNew, orgUnitId, programId, teiId, availableProgramStages }: Object) => actionCreator(newEventWidgetActionTypes.START_CREATE_NEW_AFTER_COMPLETING)({ enrollmentId, isCreateNew, orgUnitId, programId, teiId, availableProgramStages }); + +export const cleanUpEventSaveInProgress = () => + actionCreator(newEventWidgetActionTypes.CLEAN_UP_EVENT_SAVE_IN_PROGRESS)(); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.epics.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.epics.js index a747593d1c..466ceebc83 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.epics.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.epics.js @@ -1,63 +1,37 @@ // @flow import { ofType } from 'redux-observable'; import { map } from 'rxjs/operators'; -import { newEventWidgetActionTypes, saveEvents } from './validated.actions'; -import { getDataEntryKey } from '../../DataEntry/common/getDataEntryKey'; -import { getAddEventEnrollmentServerData, getNewEventClientValues } from './getConvertedAddEvent'; +import { newEventBatchActionTypes, newEventWidgetActionTypes, saveEvents } from './validated.actions'; -export const saveNewEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore) => +export const saveNewEnrollmentEventEpic = (action$: InputObservable) => action$.pipe( ofType( - newEventWidgetActionTypes.EVENT_SAVE_REQUEST, + newEventBatchActionTypes.REQUEST_SAVE_AND_SET_SUBMISSION_IN_PROGRESS, + ), + map(actionBatch => + actionBatch + .payload + .find(action => action.type === newEventWidgetActionTypes.EVENT_SAVE_REQUEST), ), map((action) => { - const state = store.value; - const { requestEvent, referralEvent, relationship } = action.payload; - const { - eventId, - formFoundation, - dataEntryId, - dataEntryItemId, - completed, - programId, - orgUnitId, - orgUnitName, - teiId, - enrollmentId, + requestEvent, + referralEvent, + relationship, + referralMode, onSaveExternal, - } = requestEvent; - - const { onSaveSuccessActionType, onSaveErrorActionType, } = action.payload; - const dataEntryKey = getDataEntryKey(dataEntryId, dataEntryItemId); - const { formClientValues, mainDataClientValues } - = getNewEventClientValues(state, dataEntryKey, formFoundation); - - const requestEventWithValues = getAddEventEnrollmentServerData({ - formFoundation, - formClientValues, - mainDataClientValues, - eventId, - programId, - orgUnitId, - orgUnitName, - teiId, - enrollmentId, - completed, - }); - const serverData = referralEvent ? { - events: [requestEventWithValues, referralEvent], + events: [requestEvent, referralEvent], relationships: [relationship], } : { - events: [requestEventWithValues], + events: [requestEvent], }; - onSaveExternal && onSaveExternal(serverData); + onSaveExternal && onSaveExternal({ referralMode, ...serverData }); return saveEvents({ serverData, onSaveSuccessActionType, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js index 60ba09e64b..11d6b93f73 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js @@ -4,26 +4,32 @@ import type { ProgramStage, RenderFoundation } from '../../../metaData'; import { typeof addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; import type { CommonValidatedProps, - ExternalSaveHandler, RulesExecutionDependenciesClientFormatted, } from '../common.types'; export type RequestEvent = { - dataEntryItemId: string, - dataEntryId: string, - formFoundation: Object, - programId: string, - orgUnitId: string, + event: string, + program: string, + programStage: string, + orgUnit: string, orgUnitName: string, - teiId: string, - enrollmentId: string, - completed?: boolean, - onSaveExternal?: ExternalSaveHandler, - onSaveSuccessActionType?: string, - onSaveErrorActionType?: string, + trackedEntity: string, + enrollment: string, + scheduledAt: string, + occurredAt: string, + dataValues: Array<{ dataElement: string, value: any }>, + notes: Array<{ value: string }>, + status: ?string, + completedAt: ?string, } +export type ReferralRefPayload = {| + getReferralValues: (eventId: string) => any, + eventHasReferralRelationship: () => boolean, + formIsValidOnSave: () => boolean, +|} + export type ContainerProps = {| ...CommonValidatedProps, orgUnit: OrgUnit, @@ -32,6 +38,8 @@ export type ContainerProps = {| export type Props = {| programName: string, programId: string, + enrollmentId: string, + eventSaveInProgress: boolean, stage: ProgramStage, formFoundation: RenderFoundation, orgUnit: OrgUnit, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js index 62d9ffccf5..edd627702f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js @@ -9,6 +9,7 @@ import { } from './WidgetEventSchedule.actions'; import { statusTypes } from '../../events/statusTypes'; import { convertCategoryOptionsToServer } from '../../converters/clientToServer'; +import { generateUID } from '../../utils/uid/generateUID'; export const scheduleEnrollmentEventEpic = (action$: InputObservable, store: ReduxStore) => action$.pipe( @@ -23,7 +24,7 @@ export const scheduleEnrollmentEventEpic = (action$: InputObservable, store: Red stageId, teiId, enrollmentId, - eventId, + eventId = generateUID(), categoryOptions, onSaveExternal, onSaveSuccessActionType, diff --git a/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.component.js b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.component.js new file mode 100644 index 0000000000..6a29db13d4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.component.js @@ -0,0 +1,74 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { + SingleSelectField, + SingleSelectOption, + spacers, +} from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import type { LinkToExistingProps } from './LinkToExisting.types'; + +const styles = () => ({ + searchRow: { + padding: spacers.dp16, + display: 'flex', + alignItems: 'center', + gap: spacers.dp16, + }, + label: { + width: '150px', + fontSize: '14px', + }, + singleSelectField: { + flexGrow: 1, + }, +}); + +export const LinkToExistingPlain = ({ + referralDataValues, + setReferralDataValues, + linkableEvents, + referralProgramStageLabel, + errorMessages, + saveAttempted, + classes, +}: LinkToExistingProps) => { + const onChange = (value) => { + setReferralDataValues({ + ...referralDataValues, + linkedEventId: value, + }); + }; + + return ( +
+

+ {i18n.t('Link to an existing {{referralProgramStageLabel}}', { + referralProgramStageLabel, + })} +

+ onChange(selected)} + placeholder={i18n.t('Choose a {{referralProgramStageLabel}}', { + referralProgramStageLabel, + })} + className={classes.singleSelectField} + error={saveAttempted && !!errorMessages.linkedEventId} + validationText={saveAttempted && errorMessages.linkedEventId} + > + {linkableEvents.map(event => ( + + ))} + +
+ ); +}; + +export const LinkToExisting = + withStyles(styles)(LinkToExistingPlain); diff --git a/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.types.js b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.types.js new file mode 100644 index 0000000000..e4ac02950b --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/LinkToExisting.types.js @@ -0,0 +1,14 @@ +// @flow + +import type { ErrorMessagesForReferral, LinkableEvent } from '../ReferralActions/ReferralActions.types'; +import type { ReferralDataValueStates } from '../WidgetReferral.types'; + +export type LinkToExistingProps = {| + referralDataValues: ReferralDataValueStates, + setReferralDataValues: (ReferralDataValueStates) => void, + linkableEvents: Array, + errorMessages: ErrorMessagesForReferral, + saveAttempted: boolean, + referralProgramStageLabel: string, + ...CssClasses, +|} diff --git a/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/index.js b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/index.js new file mode 100644 index 0000000000..ca80cff8b3 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/LinkToExisting/index.js @@ -0,0 +1,2 @@ +// @flow +export { LinkToExisting } from './LinkToExisting.component'; diff --git a/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.component.js b/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.component.js index 023db976ce..5be3387164 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.component.js +++ b/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.component.js @@ -1,13 +1,14 @@ // @flow -import React, { type ComponentType, useState } from 'react'; +import React, { type ComponentType, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; -import { Radio, colors, spacers, spacersNum } from '@dhis2/ui'; +import { Radio, colors, spacers, spacersNum, IconInfo16, IconWarning16 } from '@dhis2/ui'; import { withStyles } from '@material-ui/core'; -import { actions as ReferalActionTypes, mainOptionTranslatedTexts, referralStatus } from '../constants'; +import { actions as ReferralActionTypes, mainOptionTranslatedTexts, referralStatus } from '../constants'; import { DataSection } from '../../DataSection'; import { ReferToOrgUnit } from '../ReferToOrgUnit'; import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollection/helpers'; import type { Props } from './ReferralActions.types'; +import { LinkToExisting } from '../LinkToExisting'; const styles = () => ({ wrapper: { @@ -30,18 +31,40 @@ const styles = () => ({ flexGrow: 1, flexShrink: 0, }, + infoBox: { + margin: '8px 8px', + display: 'flex', + fontSize: '14px', + gap: '5px', + background: colors.grey100, + padding: '12px 8px', + border: `1px solid ${colors.grey600}`, + }, }); export const ReferralActionsPlain = ({ classes, type, - selectedType, + scheduledLabel, + linkableEvents, + referralDataValues, + setReferralDataValues, constraint, - ...passOnProps + currentStageLabel, + errorMessages, + saveAttempted, }: Props) => { - const [selectedAction, setSelectedAction] = useState(); const { programStage } = useProgramStageInfo(constraint?.programStage?.id); + const selectedAction = useMemo(() => referralDataValues.referralMode, [referralDataValues.referralMode]); + + const updateSelectedAction = (action: $Values) => { + setReferralDataValues(prevState => ({ + ...prevState, + referralMode: action, + })); + }; + if (!programStage) { return null; } @@ -57,8 +80,9 @@ export const ReferralActionsPlain = ({ key={key} name={`referral-action-${key}`} checked={key === selectedAction} + disabled={key === ReferralActionTypes.LINK_EXISTING_RESPONSE && !linkableEvents.length} label={mainOptionTranslatedTexts[key](programStage.stageForm.name)} - onChange={(e: Object) => setSelectedAction(e.value)} + onChange={(e: Object) => updateSelectedAction(e.value)} value={key} /> )) : null} @@ -68,11 +92,56 @@ export const ReferralActionsPlain = ({ } - {selectedAction === ReferalActionTypes.REFER_ORG && ( + {selectedAction === ReferralActionTypes.REFER_ORG && ( )} + + {selectedAction === ReferralActionTypes.ENTER_DATA && ( +
+ + {i18n.t( + 'Enter {{referralProgramStageLabel}} details in the next step after completing this {{currentStageLabel}}.', + { + referralProgramStageLabel: programStage.stageForm.name, + currentStageLabel, + }, + )} +
+ )} + + {selectedAction === ReferralActionTypes.LINK_EXISTING_RESPONSE && linkableEvents.length > 0 && ( + + )} + + {selectedAction === ReferralActionTypes.DO_NOT_LINK_RESPONSE && ( +
+ + {i18n.t( + 'This {{currentStageLabel}} will be created without a link to {{referralProgramStageLabel}}', + { + referralProgramStageLabel: programStage.stageForm.name, + currentStageLabel, + }, + )} +
+ )} ); }; diff --git a/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.types.js b/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.types.js index 175d09d520..0be5d26b9a 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.types.js +++ b/src/core_modules/capture-core/components/WidgetReferral/ReferralActions/ReferralActions.types.js @@ -1,21 +1,6 @@ // @flow import type { ReferralDataValueStates } from '../WidgetReferral.types'; -type ReferralRelationshipType = {| - id: string, - fromConstraint: { - entity: string, - programStageId?: { - id: string, - } - }, - toConstraint: { - programStage?: { - id: string, - }, - }, -|} - type Constraint = { programStage: { id: string, @@ -26,17 +11,24 @@ type Constraint = { export type ErrorMessagesForReferral = {| scheduledAt?: ?string, orgUnit?: ?string, + linkedEventId?: ?string, |} +export type LinkableEvent = { + id: string, + label: string, +} + export type Props = {| type: string, - selectedType: ReferralRelationshipType, referralDataValues: ReferralDataValueStates, + linkableEvents: Array, scheduledLabel: string, saveAttempted: boolean, errorMessages: ErrorMessagesForReferral, constraint: ?Constraint, addErrorMessage: (ErrorMessagesForReferral) => void, setReferralDataValues: (() => Object) => void, + currentStageLabel: string, ...CssClasses |} diff --git a/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.component.js b/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.component.js index 71c88f3862..fa75d696f4 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.component.js +++ b/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.component.js @@ -4,19 +4,34 @@ import { useReferral } from './useReferral'; import type { Props, ReferralDataValueStates } from './WidgetReferral.types'; import { ReferralActions } from './ReferralActions'; import { actions as ReferralModes, referralStatus } from './constants'; -import { useScheduledLabel } from './hooks/useScheduledLabel'; +import { useStageLabels } from './hooks/useStageLabels'; import type { ErrorMessagesForReferral } from './ReferralActions'; import { referralWidgetIsValid } from './referralEventIsValid/referralEventIsValid'; +import { useAvailableReferralEvents } from './hooks/useAvailableReferralEvents'; -const WidgetReferralPlain = ({ programId, programStageId, ...passOnProps }: Props, ref) => { +const WidgetReferralPlain = ({ + programId, + enrollmentId, + programStageId, + currentStageLabel, + ...passOnProps +}: Props, ref) => { const { currentReferralStatus, selectedRelationshipType, constraint } = useReferral(programStageId); + const { scheduledLabel, occurredLabel } = useStageLabels(programId, constraint?.programStage?.id); + const { linkableEvents, isLoading: isLoadingEvents } = useAvailableReferralEvents({ + stageId: constraint?.programStage?.id, + relationshipTypeId: selectedRelationshipType?.id, + scheduledLabel, + occurredLabel, + enrollmentId, + }); const [saveAttempted, setSaveAttempted] = useState(false); const [errorMessages, setErrorMessages] = useState({}); - const { scheduledLabel } = useScheduledLabel(programId, constraint?.programStage?.id); const [referralDataValues, setReferralDataValues] = useState({ referralMode: ReferralModes.REFER_ORG, scheduledAt: '', orgUnit: undefined, + linkedEventId: undefined, }); const addErrorMessage = (message: ErrorMessagesForReferral) => { @@ -34,15 +49,18 @@ const WidgetReferralPlain = ({ programId, programStageId, ...passOnProps }: Prop }; const formIsValid = useCallback(() => { - const { scheduledAt, orgUnit } = referralDataValues; + const { scheduledAt, orgUnit, linkedEventId, referralMode } = referralDataValues; return referralWidgetIsValid({ + referralMode, scheduledAt, orgUnit, + linkedEventId, setErrorMessages: addErrorMessage, }); }, [referralDataValues]); const getReferralValues = () => ({ + referralMode: referralDataValues.referralMode, referralValues: referralDataValues, referralType: selectedRelationshipType, }); @@ -60,21 +78,22 @@ const WidgetReferralPlain = ({ programId, programStageId, ...passOnProps }: Prop } }, [formIsValid, referralDataValues]); - if (!currentReferralStatus || !selectedRelationshipType) { + if (!currentReferralStatus || !selectedRelationshipType || isLoadingEvents) { return null; } return ( ); diff --git a/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.types.js b/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.types.js index 76fb570bf8..c32233a477 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.types.js +++ b/src/core_modules/capture-core/components/WidgetReferral/WidgetReferral.types.js @@ -3,7 +3,9 @@ import { actions as ReferralModes } from './constants'; export type Props = {| programId: string, + enrollmentId: string, programStageId: string, + currentStageLabel: string, |} export type ReferralDataValueStates = {| referralMode: typeof ReferralModes.REFER_ORG, @@ -13,4 +15,5 @@ export type ReferralDataValueStates = {| id: string, name: string, }, + linkedEventId: ?string, |} diff --git a/src/core_modules/capture-core/components/WidgetReferral/hooks/useAvailableReferralEvents.js b/src/core_modules/capture-core/components/WidgetReferral/hooks/useAvailableReferralEvents.js new file mode 100644 index 0000000000..95c1bb21c7 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/hooks/useAvailableReferralEvents.js @@ -0,0 +1,82 @@ +// @flow +import { useMemo } from 'react'; +import { convertDateObjectToDateFormatString } from '../../../utils/converters/date'; +import type { LinkableEvent } from '../ReferralActions/ReferralActions.types'; +import { useApiDataQuery } from '../../../utils/reactQueryHelpers'; + +type Props = { + stageId: ?string, + enrollmentId: string, + scheduledLabel: string, + occurredLabel: string, + relationshipTypeId: ?string, + enabled?: boolean, +} + + +type ReturnType = { + linkableEvents: Array, + isLoading: boolean, + isError: boolean, +} + +export const useAvailableReferralEvents = ({ + stageId, + enrollmentId, + relationshipTypeId, + scheduledLabel, + occurredLabel, + enabled = true, +}: Props): ReturnType => { + const query = useMemo(() => ({ + resource: 'tracker/events', + params: { + programStage: stageId, + enrollments: enrollmentId, + fields: 'event,occurredAt,scheduledAt,status,relationships', + }, + }), [stageId, enrollmentId]); + const { data, isLoading, isError } = useApiDataQuery>( + ['availableReferralEvents', stageId, enrollmentId, relationshipTypeId], + query, + { + enabled: !!stageId && enabled, + cacheTime: 0, + staleTime: 0, + select: (response: any) => { + const events = response?.instances.filter(instance => ['SCHEDULE', 'ACTIVE'].includes(instance.status)); + + if (events.length === 0) return []; + + return events.reduce((acc, event) => { + if (!event.relationships) return acc; + + if (event.relationships.length === 0) acc.push(event); + + const hasRelationship = !event + .relationships + .some(relationship => relationship.relationshipType === relationshipTypeId); + if (!hasRelationship) acc.push(event); + + return acc; + }, []) + .map((event) => { + const label = event.occurredAt + ? `${occurredLabel}: ${convertDateObjectToDateFormatString(new Date(event.occurredAt))}` + : `${scheduledLabel}: ${convertDateObjectToDateFormatString(new Date(event.scheduledAt))}`; + + return ({ + id: event.event, + label, + }); + }); + }, + }, + ); + + return { + linkableEvents: data ?? [], + isLoading, + isError, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetReferral/hooks/useScheduledLabel.js b/src/core_modules/capture-core/components/WidgetReferral/hooks/useScheduledLabel.js deleted file mode 100644 index 2e881ee8c8..0000000000 --- a/src/core_modules/capture-core/components/WidgetReferral/hooks/useScheduledLabel.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow -import i18n from '@dhis2/d2-i18n'; -import { getUserStorageController } from '../../../storageControllers'; -import { userStores } from '../../../storageControllers/stores'; -import { useIndexedDBQuery } from '../../../utils/reactQueryHelpers'; - -export const useScheduledLabel = (programId: string, programStageId?: string) => { - const storageController = getUserStorageController(); - - const { data, error, isLoading } = useIndexedDBQuery( - ['ScheduledAtLabel', programStageId], - () => - storageController.get(userStores.PROGRAMS, programId, { - project: ({ programStages }) => programStages - ?.find(stage => stage.id === programStageId)?.dueDateLabel, - }), - { - enabled: !!programStageId, - }, - ); - - return { - scheduledLabel: data ?? i18n.t('Scheduled date'), - isLoading, - error, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetReferral/hooks/useStageLabels.js b/src/core_modules/capture-core/components/WidgetReferral/hooks/useStageLabels.js new file mode 100644 index 0000000000..b3072022ce --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/hooks/useStageLabels.js @@ -0,0 +1,34 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { getUserStorageController } from '../../../storageControllers'; +import { userStores } from '../../../storageControllers/stores'; +import { useIndexedDBQuery } from '../../../utils/reactQueryHelpers'; + +export const useStageLabels = (programId: string, programStageId?: string) => { + const storageController = getUserStorageController(); + + const { data, error, isLoading } = useIndexedDBQuery( + // $FlowFixMe - react-query types are not up-to-date + ['programStageLabels', programStageId], + () => + storageController.get(userStores.PROGRAMS, programId, { + project: ({ programStages }) => { + const stage = programStages + ?.find(storeStage => storeStage.id === programStageId); + if (!stage) return {}; + const { displayDueDateLabel, displayExecutionDateLabel } = stage; + return { displayDueDateLabel, displayExecutionDateLabel }; + }, + }), + { + enabled: !!programStageId, + }, + ); + + return { + scheduledLabel: data?.displayDueDateLabel ?? i18n.t('Scheduled date'), + occurredLabel: data?.displayExecutionDateLabel ?? i18n.t('Report date'), + isLoading, + error, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/ValidationFunctions.js b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/ValidationFunctions.js new file mode 100644 index 0000000000..792d0dccf4 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/ValidationFunctions.js @@ -0,0 +1,71 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import { isValidDate, isValidOrgUnit } from '../../../../capture-core-utils/validators/form'; +import { actions as ReferralModes } from '../constants'; + +type Props = { + scheduledAt: ?string, + orgUnit: ?Object, + linkedEventId: ?string, + setErrorMessages: (messages: Object) => void, +}; + +export const isScheduledDateValid = (scheduledDate: string) => { + const dateFormat = systemSettingsStore.get().dateFormat; + return isValidDate(scheduledDate, dateFormat); +}; + +const referToOrgUnit = (props) => { + const { scheduledAt, orgUnit, setErrorMessages } = props ?? {}; + const scheduledAtIsValid = !!scheduledAt && isScheduledDateValid(scheduledAt); + const orgUnitIsValid = isValidOrgUnit(orgUnit); + + if (!scheduledAtIsValid) { + setErrorMessages({ + scheduledAt: i18n.t('Please provide a valid date'), + }); + } else { + setErrorMessages({ + scheduledAt: null, + }); + } + + if (!orgUnitIsValid) { + setErrorMessages({ + orgUnit: i18n.t('Please provide a valid organisation unit'), + }); + } else { + setErrorMessages({ + orgUnit: null, + }); + } + + return scheduledAtIsValid && orgUnitIsValid; +}; + +const linkToExistingResponse = (props) => { + const { linkedEventId, setErrorMessages } = props ?? {}; + const linkedEventIdIsValid = !!linkedEventId; + + if (!linkedEventIdIsValid) { + setErrorMessages({ + linkedEventId: i18n.t('Please select a valid event'), + }); + } else { + setErrorMessages({ + linkedEventId: null, + }); + } + + return linkedEventIdIsValid; +}; + + +export const ValidationFunctionsByReferralMode: { [key: string]: (props: ?Props) => boolean } = { + [ReferralModes.REFER_ORG]: props => referToOrgUnit(props), + [ReferralModes.ENTER_DATA]: () => true, + [ReferralModes.LINK_EXISTING_RESPONSE]: props => linkToExistingResponse(props), + [ReferralModes.DO_NOT_LINK_RESPONSE]: () => true, +}; + diff --git a/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.js b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.js index 075a367c7f..e69b49d591 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.js +++ b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.js @@ -1,36 +1,28 @@ // @flow -import i18n from '@dhis2/d2-i18n'; -import { isValidDate, isValidOrgUnit } from '../../../../capture-core-utils/validators/form'; -import { systemSettingsStore } from '../../../metaDataMemoryStores'; +import log from 'loglevel'; import type { ReferralIsValidProps } from './referralEventIsValid.types'; +import { errorCreator } from '../../../../capture-core-utils'; +import { ValidationFunctionsByReferralMode } from './ValidationFunctions'; -export const isScheduledDateValid = (scheduledDate: string) => { - const dateFormat = systemSettingsStore.get().dateFormat; - return isValidDate(scheduledDate, dateFormat); -}; -export const referralWidgetIsValid = ({ scheduledAt, orgUnit, setErrorMessages }: ReferralIsValidProps): boolean => { - const scheduledAtIsValid = !!scheduledAt && isScheduledDateValid(scheduledAt); - const orgUnitIsValid = isValidOrgUnit(orgUnit); - if (!scheduledAtIsValid) { - setErrorMessages({ - scheduledAt: i18n.t('Please provide a valid date'), - }); - } else { - setErrorMessages({ - scheduledAt: null, - }); - } +export const referralWidgetIsValid = ({ + referralMode, + scheduledAt, + orgUnit, + linkedEventId, + setErrorMessages, +}: ReferralIsValidProps): boolean => { + const validationFunction = ValidationFunctionsByReferralMode[referralMode]; - if (!orgUnitIsValid) { - setErrorMessages({ - orgUnit: i18n.t('Please provide a valid organisation unit'), - }); - } else { - setErrorMessages({ - orgUnit: null, - }); + if (!validationFunction) { + log.error(errorCreator('No validation function found for referral mode')); + return false; } - return scheduledAtIsValid && orgUnitIsValid; + return validationFunction({ + scheduledAt, + orgUnit, + linkedEventId, + setErrorMessages, + }); }; diff --git a/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.types.js b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.types.js index fa89ec2aaa..a575145015 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.types.js +++ b/src/core_modules/capture-core/components/WidgetReferral/referralEventIsValid/referralEventIsValid.types.js @@ -1,12 +1,15 @@ // @flow import type { ErrorMessagesForReferral } from '../ReferralActions'; +import { actions as ReferralModes } from '../constants'; export type ReferralIsValidProps = {| + referralMode: $Keys, scheduledAt: ?string, orgUnit: ?{ id: string, name: string, path: string, }, + linkedEventId: ?string, setErrorMessages: (message: ErrorMessagesForReferral) => void, |} diff --git a/src/core_modules/capture-core/components/WidgetReferral/useReferral.js b/src/core_modules/capture-core/components/WidgetReferral/useReferral.js index 033be482e3..30048dbdc3 100644 --- a/src/core_modules/capture-core/components/WidgetReferral/useReferral.js +++ b/src/core_modules/capture-core/components/WidgetReferral/useReferral.js @@ -11,7 +11,8 @@ const getRelationshipTypeFromIndexedDB = () => { }; export const useReferral = (programStageId: string) => { - const { data: relationshipTypes } = useIndexedDBQuery('relationshipTypes', + const { data: relationshipTypes } = useIndexedDBQuery( + ['relationshipTypes'], () => getRelationshipTypeFromIndexedDB(), { select: allRelationshipTypes => allRelationshipTypes ?.filter(relationshipType => relationshipType.referral && relationshipType.access.data.write) ?? [], diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js index 09ab37f7f1..fb501dcaeb 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -3,7 +3,12 @@ import { createReducerDescription } from '../../trackerRedux'; import { enrollmentSiteActionTypes } from '../../components/Pages/common/EnrollmentOverviewDomain'; import { actionTypes as enrollmentNoteActionTypes } from '../../components/WidgetEnrollmentComment/WidgetEnrollmentComment.actions'; -import { actionTypes as editEventActionTypes } from '../../components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; +import { + actionTypes as editEventActionTypes, +} from '../../components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; +import { + newEventWidgetActionTypes, +} from '../../components/WidgetEnrollmentEventNew/Validated/validated.actions'; const initialReducerValue = {}; const { @@ -17,6 +22,7 @@ const { ROLLBACK_ENROLLMENT_EVENTS, COMMIT_ENROLLMENT_EVENT, COMMIT_ENROLLMENT_EVENTS, + ADD_PERSISTED_ENROLLMENT_EVENTS, } = enrollmentSiteActionTypes; export const enrollmentDomainDesc = createReducerDescription( @@ -88,6 +94,14 @@ export const enrollmentDomainDesc = createReducerDescription( return { ...state, enrollment: { ...state.enrollment, events: enrollmentEvents } }; }, + [ADD_PERSISTED_ENROLLMENT_EVENTS]: ( + state, + { payload: { events } }, + ) => { + const enrollmentEvents = [...state.enrollment.events, ...events]; + + return { ...state, enrollment: { ...state.enrollment, events: enrollmentEvents } }; + }, [ROLLBACK_ENROLLMENT_EVENTS]: (state, { payload: { events } }) => { const comittedEventIds = events.map(event => event.event); const enrollmentEvents = state.enrollment.events.filter(event => !comittedEventIds.includes(event.event)); @@ -146,6 +160,14 @@ export const enrollmentDomainDesc = createReducerDescription( }); return { ...state, enrollment: { ...state.enrollment, events } }; }, + [newEventWidgetActionTypes.SET_SAVE_ENROLLMENT_EVENT_IN_PROGRESS]: (state, { payload }) => ({ + ...state, + eventSaveInProgress: payload, + }), + [newEventWidgetActionTypes.CLEAN_UP_EVENT_SAVE_IN_PROGRESS]: (state) => { + const { eventSaveInProgress, ...newState } = state; + return newState; + }, }, 'enrollmentDomain', initialReducerValue, diff --git a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js index 8505d4aaff..8ab794914e 100644 --- a/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js +++ b/src/core_modules/capture-core/utils/reactQueryHelpers/query/useApiDataQuery.js @@ -6,7 +6,7 @@ import type { Result } from './useMetadataQuery.types'; import { ReactQueryAppNamespace } from '../reactQueryHelpers.const'; export const useApiDataQuery = ( - queryKey: Array, + queryKey: Array, queryObject: ResourceQuery, queryOptions: UseQueryOptions, ): Result => { @@ -17,11 +17,11 @@ export const useApiDataQuery = ( [ReactQueryAppNamespace, ...queryKey], queryFn, { - ...queryOptions, refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false, staleTime: 2 * 60 * 1000, cacheTime: 5 * 60 * 1000, + ...queryOptions, }); };