diff --git a/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature index a00ec363c1..063293cc2a 100644 --- a/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature @@ -1,7 +1,5 @@ Feature: Breaking the glass page - # TODO - Flaky tests should be fixed by TECH-1662 - @skip Scenario: User with search scope access tries to access an enrollment in a protected program Given the tei created by this test is cleared from the database And the data store is clean diff --git a/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature b/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature index 8d8bc7744d..0d4355fe23 100644 --- a/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature +++ b/cypress/e2e/EnrollmentPage/EnrollmentPageNavigation.feature @@ -33,6 +33,10 @@ Feature: User interacts with Enrollment page Given you enter enrollment page by typing: #/enrollment?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=pybd813kIWx&enrollmentId=AUTO Then you should be redirect to #/enrollment?enrollmentId=FS085BEkJo2&orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW&teiId=pybd813kIWx + Scenario: Auto select orgunit when opening enrollment dashboard + Given you enter enrollment page by typing: #/enrollment?enrollmentId=avqvQMtX8DG&orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW&teiId=btoHJM9byeF + Then you should be redirect to #/enrollment?enrollmentId=avqvQMtX8DG&orgUnitId=NnGUNkc5Zq8&programId=IpHINAT79UW&teiId=btoHJM9byeF + Scenario: Reset tei Given you land on the enrollment page by having typed only the enrollmentId in the url When you reset the tei selection diff --git a/i18n/en.pot b/i18n/en.pot index baf8c8abc3..33b593d52c 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: 2024-02-09T11:22:44.980Z\n" -"PO-Revision-Date: 2024-02-09T11:22:44.980Z\n" +"POT-Creation-Date: 2024-02-14T10:01:26.732Z\n" +"PO-Revision-Date: 2024-02-14T10:01:26.732Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -662,11 +662,14 @@ msgstr "Stop using new Enrollment dashboard for {{programName}}" msgid "Opt out for {{programName}}" msgstr "Opt out for {{programName}}" +msgid "Enrollment with id \"{{enrollmentId}}\" does not exist" +msgstr "Enrollment with id \"{{enrollmentId}}\" does not exist" + msgid "Tracked entity instance with id \"{{teiId}}\" does not exist" msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" -msgid "There is an error while opening this enrollment. Please enter a valid url." -msgstr "There is an error while opening this enrollment. Please enter a valid url." +msgid "Program with id \"{{programId}}\" does not exist" +msgstr "Program with id \"{{programId}}\" does not exist" msgid "An error occurred while fetching enrollments. Please enter a valid url." msgstr "An error occurred while fetching enrollments. Please enter a valid url." diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js index 5f29c4daac..c293089be8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.actions.js @@ -2,69 +2,137 @@ import { actionCreator } from '../../../actions/actions.utils'; export const enrollmentPageActionTypes = { - INFORMATION_FETCH: 'EnrollmentPage.Fetch', - INFORMATION_USING_TEI_ID_FETCH: 'EnrollmentPage.StartFetchingUsingTeiId', - INFORMATION_USING_ENROLLMENT_ID_FETCH: 'EnrollmentPage.StartFetchingUsingEnrollmentId', - INFORMATION_LOADING_FETCH: 'EnrollmentPage.LoadingOnFetching', - INFORMATION_ERROR_FETCH: 'EnrollmentPage.ErrorOnFetching', - INFORMATION_SUCCESS_FETCH: 'EnrollmentPage.SuccessOnFetching', - - ENROLLMENTS_FETCH: 'EnrollmentPage.EnrollmentFetch', - ENROLLMENTS_ERROR_FETCH: 'EnrollmentPage.EnrollmentFetchFailure', - ENROLLMENTS_SUCCESS_FETCH: 'EnrollmentPage.EnrollmentFetchSuccess', - PAGE_OPEN: 'EnrollmentPage.Open', - PAGE_CLEAN: 'EnrollmentPage.CleanOnUnmount', - CUSTOM_PROGRAM_RESET: 'EnrollmentPage.CustomProgramReset', + PAGE_CLOSE: 'EnrollmentPage.Close', + + PROCESS_ENROLLMENT_ID: 'EnrollmentPage.EnrollmentUrlIdUpdated', + RESET_ENROLLMENT_ID: 'EnrollmentPage.ResetEnrollmentId', + FETCH_ENROLLMENT_ID: 'EnrollmentPage.FetchEnrollmentId', + VERIFY_ENROLLMENT_ID_SUCCESS: 'EnrollmentPage.VerifyEnrollmentIdSuccess', + FETCH_ENROLLMENT_ID_SUCCESS: 'EnrollmentPage.FetchEnrollmentIdSuccess', + FETCH_ENROLLMENT_ID_ERROR: 'EnrollmentPage.FetchEnrollmentIdError', + + PROCESS_TEI_ID: 'EnrollmentPage.TeiUrlIdUpdated', + RESET_TEI_ID: 'EnrollmentPage.ResetTeiId', + FETCH_TEI: 'EnrollmentPage.FetchTei', + VERIFY_FETCH_TEI_SUCCESS: 'EnrollmentPage.VerifyFetchTeiSuccess', + FETCH_TEI_SUCCESS: 'EnrollmentPage.FetchTeiSuccess', + FETCH_TEI_ERROR: 'EnrollmentPage.FetchTeiError', + + PROCESS_PROGRAM_ID: 'EnrollmentPage.ProgramUrlIdUpdated', + COMMIT_TRACKER_PROGRAM_ID: 'EnrollmentPage.CommitTrackerProgramId', + COMMIT_NON_TRACKER_PROGRAM_ID: 'EnrollmentPage.CommitNonTrackerProgramId', + PROGRAM_ID_ERROR: 'EnrollmentPage.ProgramIdError', + + FETCH_ENROLLMENTS: 'EnrollmentPage.FetchEnrollments', + VERIFY_FETCHED_ENROLLMENTS: 'EnrollmentPage.VerifyFetchedEnrollments', + FETCH_ENROLLMENTS_ERROR: 'EnrollmentPage.FetchEnrollmentsError', + FETCH_ENROLLMENTS_SUCCESS: 'EnrollmentPage.FetchEnrollmentsSuccess', DEFAULT_VIEW: 'EnrollmentPage.DefaultView', + LOADING_VIEW: 'EnrollmentPage.LoadingView', MISSING_MESSAGE_VIEW: 'EnrollmentPage.MissingMessageView', + ERROR_VIEW: 'EnrollmentPage.ErrorView', + CLEAR_ERROR_VIEW: 'EnrollmentPage.ClearErrorView', DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', UPDATE_ENROLLMENT_DATE: 'EnrollmentPage.UpdateEnrollmentDate', }; -export const fetchEnrollmentPageInformation = () => - actionCreator(enrollmentPageActionTypes.INFORMATION_FETCH)(); -export const startFetchingTeiFromTeiId = () => - actionCreator(enrollmentPageActionTypes.INFORMATION_USING_TEI_ID_FETCH)(); +type IdSuite = { + teiId?: ?string, + programId?: ?string, +}; -export const startFetchingTeiFromEnrollmentId = () => - actionCreator(enrollmentPageActionTypes.INFORMATION_USING_ENROLLMENT_ID_FETCH)(); +export const openEnrollmentPage = () => + actionCreator(enrollmentPageActionTypes.PAGE_OPEN)(); -export const showLoadingViewOnEnrollmentPage = () => - actionCreator(enrollmentPageActionTypes.INFORMATION_LOADING_FETCH)(); +export const closeEnrollmentPage = () => + actionCreator(enrollmentPageActionTypes.PAGE_CLOSE)(); -export const showDefaultViewOnEnrollmentPage = () => - actionCreator(enrollmentPageActionTypes.DEFAULT_VIEW)(); +// enrollmentId +export const changedEnrollmentId = (enrollmentId: ?string) => + actionCreator(enrollmentPageActionTypes.PROCESS_ENROLLMENT_ID)(enrollmentId); -export const showMissingMessageViewOnEnrollmentPage = () => - actionCreator(enrollmentPageActionTypes.MISSING_MESSAGE_VIEW)(); +export const resetEnrollmentId = (payload: IdSuite) => + actionCreator(enrollmentPageActionTypes.RESET_ENROLLMENT_ID)(payload); -export const showErrorViewOnEnrollmentPage = ({ error }: { error: string }) => - actionCreator(enrollmentPageActionTypes.INFORMATION_ERROR_FETCH)({ error }); +export const fetchEnrollmentId = (enrollmentId: string) => + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID)({ enrollmentId }); + +export const verifyEnrollmentIdSuccess = ({ enrollmentId, trackedEntity, program }: Object) => + actionCreator(enrollmentPageActionTypes.VERIFY_ENROLLMENT_ID_SUCCESS)({ enrollmentId, teiId: trackedEntity, programId: program }); + +export const fetchEnrollmentIdSuccess = (payload: IdSuite) => + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_SUCCESS)(payload); + +export const fetchEnrollmentIdError = (enrollmentId: string) => + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_ERROR)({ enrollmentId }); + +// teiId +export const changedTeiId = (payload: IdSuite) => + actionCreator(enrollmentPageActionTypes.PROCESS_TEI_ID)(payload); + +export const resetTeiId = () => + actionCreator(enrollmentPageActionTypes.RESET_TEI_ID)(); + +export const fetchTei = (payload: IdSuite) => + actionCreator(enrollmentPageActionTypes.FETCH_TEI)(payload); + +export const verifyFetchTeiSuccess = (payload: { ...IdSuite, teiDisplayName: string, tetId: string }) => + actionCreator(enrollmentPageActionTypes.VERIFY_FETCH_TEI_SUCCESS)(payload); -export const successfulFetchingEnrollmentPageInformationFromUrl = ({ teiDisplayName, tetId }: Object) => - actionCreator(enrollmentPageActionTypes.INFORMATION_SUCCESS_FETCH)( - { teiDisplayName, tetId }); +export const fetchTeiSuccess = (payload: { ...IdSuite, teiDisplayName: string, tetId: string }) => + actionCreator(enrollmentPageActionTypes.FETCH_TEI_SUCCESS)(payload); +export const fetchTeiError = (teiId: string) => + actionCreator(enrollmentPageActionTypes.FETCH_TEI_ERROR)({ teiId }); + +// programId +export const changedProgramId = (payload: IdSuite) => + actionCreator(enrollmentPageActionTypes.PROCESS_PROGRAM_ID)(payload); + +export const commitTrackerProgramId = (programId: string) => + actionCreator(enrollmentPageActionTypes.COMMIT_TRACKER_PROGRAM_ID)({ programId }); + +export const commitNonTrackerProgramId = (programId: string) => + actionCreator(enrollmentPageActionTypes.COMMIT_NON_TRACKER_PROGRAM_ID)({ programId }); + +export const programIdError = (programId: string) => + actionCreator(enrollmentPageActionTypes.PROGRAM_ID_ERROR)({ programId }); + +// enrollments export const fetchEnrollments = () => - actionCreator(enrollmentPageActionTypes.ENROLLMENTS_FETCH)(); + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENTS)(); + +export const verifyFetchedEnrollments = ({ teiId, programId, action }: Object) => + actionCreator(enrollmentPageActionTypes.VERIFY_FETCHED_ENROLLMENTS)({ teiId, programId, action }); -export const updateEnrollmentAccessLevel = ({ programId, accessLevel }: { programId: string, accessLevel: string }) => - actionCreator(enrollmentPageActionTypes.ENROLLMENTS_ERROR_FETCH)({ programId, accessLevel }); +export const fetchEnrollmentsError = ({ accessLevel }: { accessLevel: string }) => + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENTS_ERROR)({ accessLevel }); -export const saveEnrollments = ({ programId, enrollments }: any) => - actionCreator(enrollmentPageActionTypes.ENROLLMENTS_SUCCESS_FETCH)({ programId, enrollments }); +export const saveEnrollments = ({ enrollments }: any) => + actionCreator(enrollmentPageActionTypes.FETCH_ENROLLMENTS_SUCCESS)({ enrollments }); -export const openEnrollmentPage = ({ programId, orgUnitId, teiId, enrollmentId }: Object) => - actionCreator(enrollmentPageActionTypes.PAGE_OPEN)({ programId, orgUnitId, teiId, enrollmentId }); +// Page status +export const showDefaultViewOnEnrollmentPage = () => + actionCreator(enrollmentPageActionTypes.DEFAULT_VIEW)(); + +export const showLoadingViewOnEnrollmentPage = () => + actionCreator(enrollmentPageActionTypes.LOADING_VIEW)(); + +export const showMissingMessageViewOnEnrollmentPage = () => + actionCreator(enrollmentPageActionTypes.MISSING_MESSAGE_VIEW)(); + +export const showErrorViewOnEnrollmentPage = ({ error }: { error: string }) => + actionCreator(enrollmentPageActionTypes.ERROR_VIEW)({ error }); -export const cleanEnrollmentPage = () => - actionCreator(enrollmentPageActionTypes.PAGE_CLEAN)(); +export const clearErrorView = () => + actionCreator(enrollmentPageActionTypes.CLEAR_ERROR_VIEW)(); +// Mutations export const deleteEnrollment = ({ enrollmentId }: { enrollmentId: string }) => actionCreator(enrollmentPageActionTypes.DELETE_ENROLLMENT)({ enrollmentId, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.constants.js index 951e9e9184..73a084f8dd 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.constants.js @@ -6,6 +6,12 @@ export const enrollmentPageStatuses = { MISSING_SELECTIONS: 'MISSING_SELECTIONS', }; +export const selectionStatus = { + READY: 'READY', + ERROR: 'ERROR', + LOADING: 'LOADING', +}; + export const enrollmentAccessLevels = { FULL_ACCESS: 'FULL_ACCESS', LIMITED_ACCESS: 'LIMITED_ACCESS', diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.container.js index 51186e7fc9..bacc367ac1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.container.js @@ -3,12 +3,13 @@ import React, { useEffect, useMemo } from 'react'; import type { ComponentType } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { EnrollmentPageComponent } from './EnrollmentPage.component'; -import type { EnrollmentPageStatus } from './EnrollmentPage.types'; import { - cleanEnrollmentPage, - fetchEnrollmentPageInformation, - fetchEnrollments, - updateEnrollmentAccessLevel, + openEnrollmentPage, + closeEnrollmentPage, + changedEnrollmentId, + changedTeiId, + changedProgramId, + resetTeiId, showDefaultViewOnEnrollmentPage, showMissingMessageViewOnEnrollmentPage, showLoadingViewOnEnrollmentPage, @@ -16,7 +17,7 @@ import { import { scopeTypes } from '../../../metaData/helpers/constants'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useEnrollmentInfo } from './useEnrollmentInfo'; -import { enrollmentPageStatuses, enrollmentAccessLevels } from './EnrollmentPage.constants'; +import { enrollmentPageStatuses } from './EnrollmentPage.constants'; import { getScopeInfo } from '../../../metaData'; import { buildEnrollmentsAsOptions, @@ -30,7 +31,10 @@ const useComponentLifecycle = () => { const { scopeType } = useScopeInfo(programId); const { setEnrollmentId } = useSetEnrollmentId(); - const { enrollmentAccessLevel, programId: enrollmentProgramId } = useSelector(({ enrollmentPage }) => enrollmentPage); + const { + enrollmentPageStatus, + programId: enrollmentProgramId, + } = useSelector(({ enrollmentPage }) => enrollmentPage); const { programHasEnrollments, enrollmentsOnProgramContainEnrollmentId, autoEnrollmentId } = useEnrollmentInfo(enrollmentId, programId, teiId); useEffect(() => { @@ -39,7 +43,7 @@ const useComponentLifecycle = () => { setEnrollmentId({ enrollmentId: autoEnrollmentId, shouldReplaceHistory: true }); } else if (selectedProgramIsTracker && programHasEnrollments && enrollmentsOnProgramContainEnrollmentId) { dispatch(showDefaultViewOnEnrollmentPage()); - } else if (programId === enrollmentProgramId) { + } else if (programId === enrollmentProgramId && enrollmentPageStatus !== enrollmentPageStatuses.LOADING) { dispatch(showMissingMessageViewOnEnrollmentPage()); } else { dispatch(showLoadingViewOnEnrollmentPage()); @@ -54,18 +58,18 @@ const useComponentLifecycle = () => { scopeType, enrollmentId, autoEnrollmentId, - enrollmentAccessLevel, + enrollmentPageStatus, enrollmentProgramId, ]); - useEffect(() => () => dispatch(cleanEnrollmentPage()), [dispatch, teiId]); + useEffect(() => () => dispatch(closeEnrollmentPage()), [dispatch]); }; -// dirty fix for scenarios where you deselect the program. -// This should be removed as part of fixing the url sync issue, https://jira.dhis2.org/browse/TECH-580 const useComputedEnrollmentPageStatus = () => { - const enrollmentPageStatus: EnrollmentPageStatus = - useSelector(({ enrollmentPage }) => enrollmentPage.enrollmentPageStatus); + const { + enrollmentPageStatus, + programId: reduxProgramId, + } = useSelector(({ enrollmentPage }) => enrollmentPage); const { teiId, programId, enrollmentId } = useLocationQuery(); const { scopeType } = useScopeInfo(programId); @@ -75,7 +79,7 @@ const useComputedEnrollmentPageStatus = () => { return enrollmentPageStatuses.MISSING_SELECTIONS; } if (enrollmentPageStatus === enrollmentPageStatuses.DEFAULT && - !(programId && teiId && enrollmentId)) { + !(programId && teiId && enrollmentId && programId === reduxProgramId)) { return enrollmentPageStatuses.LOADING; } return enrollmentPageStatus; @@ -85,6 +89,7 @@ const useComputedEnrollmentPageStatus = () => { enrollmentId, teiId, programId, + reduxProgramId, ]); }; @@ -98,25 +103,12 @@ export const EnrollmentPage: ComponentType<{||}> = () => { const enrollmentsAsOptions = buildEnrollmentsAsOptions(enrollments, programId); useEffect(() => { - dispatch(fetchEnrollmentPageInformation()); - }, - [ - dispatch, - teiId, - ]); + dispatch(openEnrollmentPage()); + }, [dispatch]); - useEffect(() => { - programId ? - dispatch(fetchEnrollments()) : - dispatch(updateEnrollmentAccessLevel({ - programId, - accessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, - })); - }, - [ - dispatch, - programId, - ]); + useEffect(() => { dispatch(changedEnrollmentId(enrollmentId)); }, [dispatch, enrollmentId]); + useEffect(() => { dispatch(teiId ? changedTeiId({ teiId }) : resetTeiId()); }, [dispatch, teiId]); + useEffect(() => { dispatch(changedProgramId({ programId })); }, [dispatch, programId]); const error: boolean = useSelector(({ activePage }) => activePage.selectionsError && activePage.selectionsError.error); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index 6d905e0188..ef010911d3 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -1,23 +1,35 @@ // @flow import { ofType } from 'redux-observable'; -import { catchError, flatMap, map, startWith } from 'rxjs/operators'; +import { catchError, concatMap, map, filter } from 'rxjs/operators'; +import { from, of, EMPTY } from 'rxjs'; import i18n from '@dhis2/d2-i18n'; -import { from, of } from 'rxjs'; import moment from 'moment'; import { enrollmentPageActionTypes, - showErrorViewOnEnrollmentPage, - showLoadingViewOnEnrollmentPage, - successfulFetchingEnrollmentPageInformationFromUrl, - updateEnrollmentAccessLevel, + resetEnrollmentId, + fetchEnrollmentId, + verifyEnrollmentIdSuccess, + fetchEnrollmentIdSuccess, + fetchEnrollmentIdError, + fetchTei, + verifyFetchTeiSuccess, + fetchTeiSuccess, + fetchTeiError, + commitTrackerProgramId, + commitNonTrackerProgramId, + programIdError, + fetchEnrollments, + verifyFetchedEnrollments, saveEnrollments, - openEnrollmentPage, - startFetchingTeiFromEnrollmentId, - startFetchingTeiFromTeiId, + fetchEnrollmentsError, + showErrorViewOnEnrollmentPage, + clearErrorView, } from './EnrollmentPage.actions'; -import { enrollmentAccessLevels, serverErrorMessages } from './EnrollmentPage.constants'; +import { enrollmentAccessLevels, serverErrorMessages, selectionStatus } from './EnrollmentPage.constants'; import { buildUrlQueryString, getLocationQuery } from '../../../utils/routing'; import { deriveTeiName } from '../common/EnrollmentOverviewDomain/useTeiDisplayName'; +import { getScopeInfo } from '../../../metaData'; +import { scopeTypes } from '../../../metaData/helpers/constants'; const sortByDate = (enrollments = []) => enrollments.sort((a, b) => moment.utc(b.enrolledAt).diff(moment.utc(a.enrolledAt))); @@ -30,6 +42,14 @@ const teiQuery = id => ({ }, }); +const enrollmentIdQuery = enrollmentId => ({ + resource: 'tracker/enrollments', + id: enrollmentId, + params: { + fields: ['trackedEntity', 'program'], + }, +}); + const enrollmentsQuery = (teiId, programId) => ({ resource: 'tracker/trackedEntities', id: teiId, @@ -39,172 +59,288 @@ const enrollmentsQuery = (teiId, programId) => ({ }, }); -const captureScopeQuery = () => ({ - resource: 'organisationUnits', +const programOwnersQuery = (teiId, programId) => ({ + resource: 'tracker/trackedEntities', + id: teiId, params: { - paging: false, - userOnly: true, - fields: 'id', + program: programId, + fields: ['programOwners[program,orgUnit]'], }, }); -const ancestorsQuery = orgUnitId => ({ +const captureScopeQuery = orgUnitId => ({ resource: 'organisationUnits', - id: orgUnitId, params: { - fields: 'ancestors', + query: orgUnitId, + withinUserHierarchy: true, + fields: 'id', }, }); -const inCaptureScope = (querySingleResource, orgUnitId) => - Promise.all([ - querySingleResource(captureScopeQuery()), - querySingleResource(ancestorsQuery(orgUnitId)), - ]).then(([{ organisationUnits }, { ancestors }]) => { - ancestors.push({ id: orgUnitId }); - return ancestors.some(({ id: ancestorId }) => organisationUnits.some(({ id }) => ancestorId === id)); - }).catch(error => console.log(error)); - -const autoSelectEnrollment = ( - programId: string, - orgUnitId: string, - teiId: string, -): InputObservable => { - if (teiId && programId) { - return of(openEnrollmentPage({ - programId, - orgUnitId, - teiId, - enrollmentId: 'AUTO', - })); - } - return of(startFetchingTeiFromTeiId()); +const deselectTei = (history) => { + const { programId, orgUnitId } = getLocationQuery(); + history.push(`/?${buildUrlQueryString({ programId, orgUnitId })}`); + return false; }; -const fetchTeiStream = (teiId, querySingleResource) => - from(querySingleResource(teiQuery(teiId))) - .pipe( - map(({ attributes, trackedEntityType }) => { - const teiDisplayName = deriveTeiName(attributes, trackedEntityType, teiId); +// Check fetch status +const enrollmentIdReady = (store: ReduxStore): boolean => { + const { fetchStatus } = store.value.enrollmentPage; + return fetchStatus.enrollmentId === selectionStatus.READY; +}; + +const teiIdReady = (store: ReduxStore): boolean => { + const { teiId, fetchStatus } = store.value.enrollmentPage; + return teiId && fetchStatus.teiId === selectionStatus.READY; +}; + +const programIdReady = (store: ReduxStore): boolean => { + const { fetchStatus } = store.value.enrollmentPage; + return fetchStatus.programId === selectionStatus.READY; +}; + +const enrollmentIdLoaded = (enrollmentId: string, enrollments: ?Array) => + enrollments && enrollments.some(enrollment => enrollment.enrollment === enrollmentId); + + +// The verification epics which are triggered by the completion of +// async requests (e.g. verifyEnrollmentIdSuccessEpic) are not subject +// to race conditions with other async requests because the chain of +// epics and reducers triggered by the completion is resolved +// synchronously before moving on to the next completed request. + +// Note that the reason for doing the verification in a separate epic +// is to make sure we are using the most recent version of the redux +// store (but we might in fact have access to the most recent state +// using the old store object as well). + +// Epics for enrollmentId +export const changedEnrollmentIdEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType(enrollmentPageActionTypes.PROCESS_ENROLLMENT_ID), + filter(({ payload: enrollmentId }) => + ((!enrollmentId || enrollmentId === 'AUTO') ? + store.value.enrollmentPage.enrollmentId : + !enrollmentIdLoaded(enrollmentId, store.value.enrollmentPage.enrollments))), + map(({ payload: enrollmentId }) => { + if (!enrollmentId || enrollmentId === 'AUTO') { + const { programId, teiId } = getLocationQuery(); + return resetEnrollmentId({ programId, teiId }); + } + return fetchEnrollmentId(enrollmentId); + }), + ); + +export const fetchEnrollmentIdEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID), + concatMap(({ payload: { enrollmentId } }) => + querySingleResource(enrollmentIdQuery(enrollmentId)) + .then(result => verifyEnrollmentIdSuccess({ enrollmentId, ...result })) + .catch(() => fetchEnrollmentIdError(enrollmentId))), + ); + +export const verifyEnrollmentIdSuccessEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType(enrollmentPageActionTypes.VERIFY_ENROLLMENT_ID_SUCCESS), + filter(({ payload }) => payload.enrollmentId === store.value.enrollmentPage.enrollmentId), + map(({ payload }) => fetchEnrollmentIdSuccess(payload)), + ); + +export const enrollmentIdErrorEpic = (action$: InputObservable) => + action$.pipe( + ofType(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_ERROR), + map(({ payload: { enrollmentId } }) => + showErrorViewOnEnrollmentPage({ error: i18n.t('Enrollment with id "{{enrollmentId}}" does not exist', { enrollmentId }) })), + ); - return successfulFetchingEnrollmentPageInformationFromUrl({ +// Epics for teiId +export const changedTeiIdEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType( + enrollmentPageActionTypes.PROCESS_TEI_ID, + enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_SUCCESS, + enrollmentPageActionTypes.RESET_ENROLLMENT_ID), + filter(({ payload: { teiId } }) => + enrollmentIdReady(store) && + teiId && store.value.enrollmentPage.teiId !== teiId), + map(({ payload }) => fetchTei(payload)), + ); + +export const resetTeiIdEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.RESET_TEI_ID), + filter(() => + (({ fetchStatus }) => + fetchStatus.enrollmentId !== selectionStatus.LOADING && + fetchStatus.teiId !== selectionStatus.LOADING && + deselectTei(history) + )(store.value.enrollmentPage)), + ); + +export const fetchTeiIdEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.FETCH_TEI), + concatMap(({ payload: { teiId, programId } }) => from(querySingleResource(teiQuery(teiId)) + .then(({ attributes, trackedEntityType }) => { + const teiDisplayName = deriveTeiName(attributes, trackedEntityType, teiId); + return verifyFetchTeiSuccess({ teiDisplayName, tetId: trackedEntityType, + teiId, + programId, }); - }), - catchError(() => { - const error = i18n.t('Tracked entity instance with id "{{teiId}}" does not exist', { teiId }); - return of(showErrorViewOnEnrollmentPage({ error })); - }), - ); + }) + .catch(() => fetchTeiError(teiId)))), + ); -export const fetchEnrollmentPageInformationFromUrlEpic = (action$: InputObservable) => +export const verifyTeiFetchSuccessEpic = (action$: InputObservable, store: ReduxStore) => action$.pipe( - ofType(enrollmentPageActionTypes.INFORMATION_FETCH), - map(() => { - const { enrollmentId, teiId } = getLocationQuery(); - if (enrollmentId) { - return startFetchingTeiFromEnrollmentId(); - } else if (teiId) { - return startFetchingTeiFromTeiId(); - } - const error = i18n.t('There is an error while opening this enrollment. Please enter a valid url.'); - return showErrorViewOnEnrollmentPage({ error }); - }), + ofType(enrollmentPageActionTypes.VERIFY_FETCH_TEI_SUCCESS), + filter(() => enrollmentIdReady(store)), + map(({ payload }) => fetchTeiSuccess(payload)), ); -export const startFetchingTeiFromEnrollmentIdEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const fetchTeiErrorEpic = (action$: InputObservable) => action$.pipe( - ofType(enrollmentPageActionTypes.INFORMATION_USING_ENROLLMENT_ID_FETCH), - flatMap(() => { - const { enrollmentId, programId, orgUnitId, teiId } = getLocationQuery(); - if (enrollmentId === 'AUTO') { - return autoSelectEnrollment(programId, orgUnitId, teiId); + ofType(enrollmentPageActionTypes.FETCH_TEI_ERROR), + map(({ payload: { teiId } }) => showErrorViewOnEnrollmentPage({ error: i18n.t('Tracked entity instance with id "{{teiId}}" does not exist', { teiId }) })), + ); + +// Epics for programId +export const changedProgramIdEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType( + enrollmentPageActionTypes.PROCESS_PROGRAM_ID, + enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_SUCCESS, + enrollmentPageActionTypes.RESET_ENROLLMENT_ID), + filter(({ payload: { programId } }) => + enrollmentIdReady(store) && + store.value.enrollmentPage.programId !== programId), + map(({ payload: { programId } }) => { + const { scopeType } = getScopeInfo(programId); + if (scopeType === scopeTypes.TRACKER_PROGRAM) { + return commitTrackerProgramId(programId); + } else if (!programId || scopeType === scopeTypes.EVENT_PROGRAM) { + return commitNonTrackerProgramId(programId); } - return from(querySingleResource({ resource: 'tracker/enrollments', id: enrollmentId })) - .pipe( - flatMap(({ trackedEntity, program, orgUnit }) => - from(inCaptureScope(querySingleResource, orgUnit)) - .pipe( - map(programOwnerInCaptureScope => - openEnrollmentPage({ - programId: program, - teiId: trackedEntity, - orgUnitId: programOwnerInCaptureScope ? orgUnit : orgUnitId, - enrollmentId, - }), - ))), - catchError(() => of(startFetchingTeiFromTeiId())), - startWith(showLoadingViewOnEnrollmentPage()), - ); + return programIdError(programId); }), ); -export const startFetchingTeiFromTeiIdEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +export const programIdErrorEpic = (action$: InputObservable) => action$.pipe( - ofType(enrollmentPageActionTypes.INFORMATION_USING_TEI_ID_FETCH), - flatMap(() => { - const { teiId } = getLocationQuery(); - - return fetchTeiStream(teiId, querySingleResource); - }), + ofType(enrollmentPageActionTypes.PROGRAM_ID_ERROR), + map(({ payload: { programId } }) => + showErrorViewOnEnrollmentPage({ error: i18n.t('Program with id "{{programId}}" does not exist', { programId }) })), ); -export const fetchEnrollmentsEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => +// Epics for enrollments +export const teiOrProgramChangeEpic = (action$: InputObservable, store: ReduxStore, { history }: ApiUtils) => action$.pipe( - ofType(enrollmentPageActionTypes.ENROLLMENTS_FETCH, enrollmentPageActionTypes.INFORMATION_SUCCESS_FETCH), - flatMap(() => { - const { teiId, programId } = getLocationQuery(); + ofType( + enrollmentPageActionTypes.FETCH_TEI_SUCCESS, + enrollmentPageActionTypes.COMMIT_TRACKER_PROGRAM_ID, + enrollmentPageActionTypes.COMMIT_NON_TRACKER_PROGRAM_ID), + filter(() => store.value.enrollmentPage.pageOpen), + map(() => { + // Update url + const { teiId, programId } = store.value.enrollmentPage; + history.push(`/enrollment?${buildUrlQueryString({ ...getLocationQuery(), teiId, programId })}`); - if (!teiId || !programId) { - return of(updateEnrollmentAccessLevel({ programId, accessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS })); + if (teiIdReady(store) && programIdReady(store)) { + return fetchEnrollments(); + } else if (enrollmentIdReady(store) && teiIdReady(store)) { + return fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS }); } + return null; + }), + filter(action => action), + ); + +export const fetchEnrollmentsEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.FETCH_ENROLLMENTS), + concatMap(() => { + const { teiId, programId } = store.value.enrollmentPage; return from(querySingleResource(enrollmentsQuery(teiId, programId))) .pipe( map(({ enrollments }) => { const enrollmentsSortedByDate = sortByDate(enrollments .filter(enrollment => enrollment.program === programId)); - return saveEnrollments({ programId, enrollments: enrollmentsSortedByDate }); + return saveEnrollments({ enrollments: enrollmentsSortedByDate }); }), catchError((error) => { if (error.message) { if (error.message.includes(serverErrorMessages.OWNERSHIP_ACCESS_PARTIALLY_DENIED)) { - return of(updateEnrollmentAccessLevel({ programId, accessLevel: enrollmentAccessLevels.LIMITED_ACCESS })); + return of(fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.LIMITED_ACCESS })); } else if (error.message.includes(serverErrorMessages.OWNERSHIP_ACCESS_DENIED)) { - return of(updateEnrollmentAccessLevel({ programId, accessLevel: enrollmentAccessLevels.LIMITED_ACCESS })); // Todo: Change to NO_ACCESS + return of(fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.LIMITED_ACCESS })); } else if (error.message.includes(serverErrorMessages.PROGRAM_ACCESS_CLOSED)) { - return of(updateEnrollmentAccessLevel({ programId, accessLevel: enrollmentAccessLevels.NO_ACCESS })); + return of(fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.NO_ACCESS })); } else if (error.message.includes(serverErrorMessages.ORGUNIT_OUT_OF_SCOPE)) { - return of(updateEnrollmentAccessLevel({ programId, accessLevel: enrollmentAccessLevels.NO_ACCESS })); + return of(fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.NO_ACCESS })); } } const errorMessage = i18n.t('An error occurred while fetching enrollments. Please enter a valid url.'); return of(showErrorViewOnEnrollmentPage({ error: errorMessage })); }), + map(action => verifyFetchedEnrollments({ teiId, programId, action })), ); }), ); -export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, history }: ApiUtils) => +export const verifyFetchedEnrollmentsEpic = (action$: InputObservable, store: ReduxStore) => action$.pipe( - ofType(enrollmentPageActionTypes.PAGE_OPEN), - flatMap(({ payload: { enrollmentId, programId, orgUnitId, teiId } }) => { - const { - enrollmentId: queryEnrollment, - orgUnitId: queryOrgUnitId, - programId: queryProgramId, - teiId: queryTeiId, - } = getLocationQuery(); + ofType(enrollmentPageActionTypes.VERIFY_FETCHED_ENROLLMENTS), + filter(() => enrollmentIdReady(store)), + filter(({ payload: { teiId: fetchedTeiId, programId: fetchedProgramId } }) => { + const { teiId, programId } = store.value.enrollmentPage; + return fetchedTeiId === teiId && fetchedProgramId === programId; + }), + map(({ payload: { action } }) => action), + ); - if (enrollmentId !== queryEnrollment || - orgUnitId !== queryOrgUnitId || - programId !== queryProgramId || - teiId !== queryTeiId) { - history.push(`/enrollment?${buildUrlQueryString({ programId, orgUnitId, teiId, enrollmentId })}`); - } - return fetchTeiStream(teiId, querySingleResource); - }, - ), +// Auto-switch orgUnit epic +export const autoSwitchOrgUnitEpic = (action$: InputObservable, store: ReduxStore, { querySingleResource, history }: ApiUtils) => + action$.pipe( + ofType(enrollmentPageActionTypes.FETCH_ENROLLMENTS), + map(() => (({ teiId, programId }) => ({ teiId, programId }))(store.value.enrollmentPage)), + concatMap(({ teiId, programId }) => from(querySingleResource(programOwnersQuery(teiId, programId))) + .pipe( + map(({ programOwners }) => programOwners.find(programOwner => programOwner.program === programId)), + filter(programOwner => programOwner), + concatMap(programOwner => from(querySingleResource(captureScopeQuery(programOwner.orgUnit))) + .pipe( + concatMap(({ organisationUnits }) => { + if (organisationUnits.length > 0 && store.value.enrollmentPage.pageOpen) { + // Update orgUnitId in url + const { orgUnitId, ...restOfQueries } = getLocationQuery(); + history.push(`/enrollment?${buildUrlQueryString({ ...restOfQueries, orgUnitId: programOwner.orgUnit })}`); + } + return EMPTY; + }), + catchError(() => EMPTY), + )), + catchError(() => EMPTY), + )), ); +// Manage error messages for unsuccessful data fetches. +export const clearErrorViewEpic = (action$: InputObservable, store: ReduxStore) => + action$.pipe( + ofType( + enrollmentPageActionTypes.RESET_ENROLLMENT_ID, + enrollmentPageActionTypes.FETCH_ENROLLMENT_ID, + enrollmentPageActionTypes.FETCH_TEI, + enrollmentPageActionTypes.COMMIT_TRACKER_PROGRAM_ID), + filter(() => store.value.activePage.selectionsError), + filter(() => { + const fetchStatus = store.value.enrollmentPage.fetchStatus; + return fetchStatus.enrollmentId !== selectionStatus.ERROR && + fetchStatus.programId !== selectionStatus.ERROR && + fetchStatus.teiId !== selectionStatus.ERROR; + }), + map(() => clearErrorView()), + ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/useEnrollmentInfo.js b/src/core_modules/capture-core/components/Pages/Enrollment/useEnrollmentInfo.js index 80a6fa1d78..e037180c46 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/useEnrollmentInfo.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/useEnrollmentInfo.js @@ -28,14 +28,11 @@ const getSuitableEnrollmentId = (enrollments, teiId) => { }; export const useEnrollmentInfo = (enrollmentId: string, programId: string, teiId: string) => { - const enrollments = useSelector(({ enrollmentPage }) => enrollmentPage.enrollments); - const tetId = useSelector(({ enrollmentPage }) => enrollmentPage.tetId); - const programHasEnrollments = enrollments && enrollments.some(({ program }) => programId === program); + const { enrollments, tetId } = useSelector(({ enrollmentPage }) => enrollmentPage); + const programHasEnrollments = enrollments && enrollments.length > 0; const programHasActiveEnrollments = programHasEnrollments && enrollments - .filter(({ program }) => program === programId) .some(({ status }) => status === 'ACTIVE'); const enrollmentsOnProgramContainEnrollmentId = enrollments && enrollments - .filter(({ program }) => program === programId) .some(({ enrollment }) => enrollmentId === enrollment); const onlyEnrollOnce = programId && programCollection.get(programId)?.onlyEnrollOnce; const enrollmentsInProgram = enrollments && enrollments.filter(({ program }) => program === programId); diff --git a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js index 0aba4ccd71..6b2e1f68dc 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js +++ b/src/core_modules/capture-core/components/ScopeSelector/ScopeSelector.container.js @@ -7,10 +7,10 @@ import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { resetOrgUnitIdFromScopeSelector } from './ScopeSelector.actions'; -const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName, displayName) => { +const deriveReadiness = (lockedSelectorLoads, selectedOrgUnitId, selectedOrgUnitName, displayName, ouNameError) => { // because we want the orgUnit to be fetched and stored // before allowing the user to view the locked selector - if (selectedOrgUnitId && (!selectedOrgUnitName || selectedOrgUnitName !== displayName)) { + if (!ouNameError && selectedOrgUnitId && (!selectedOrgUnitName || selectedOrgUnitName !== displayName)) { return false; } return !lockedSelectorLoads; @@ -33,7 +33,7 @@ export const ScopeSelector: ComponentType = ({ }) => { const dispatch = useDispatch(); const [selectedOrgUnit, setSelectedOrgUnit] = useState({ name: undefined, id: selectedOrgUnitId }); - const { displayName } = useOrgUnitName(selectedOrgUnit.id); + const { displayName, error: ouNameError } = useOrgUnitName(selectedOrgUnit.id); useEffect(() => { if (displayName && selectedOrgUnit.name !== displayName) { @@ -42,7 +42,7 @@ export const ScopeSelector: ComponentType = ({ }, [displayName, selectedOrgUnit, setSelectedOrgUnit]); useEffect(() => { - if (selectedOrgUnitId && !selectedOrgUnit.id) { + if (selectedOrgUnitId && selectedOrgUnit.id !== selectedOrgUnitId) { selectedOrgUnitId && setSelectedOrgUnit(prevSelectedOrgUnit => ({ ...prevSelectedOrgUnit, id: selectedOrgUnitId })); } }, [selectedOrgUnitId, selectedOrgUnit, setSelectedOrgUnit]); @@ -58,7 +58,7 @@ export const ScopeSelector: ComponentType = ({ previousOrgUnitId: app.previousOrgUnitId, } )); - const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnit.id, selectedOrgUnit.name, displayName); + const ready = deriveReadiness(lockedSelectorLoads, selectedOrgUnit.id, selectedOrgUnit.name, displayName, ouNameError); return ( = ({ onSetProgramId={onSetProgramId} onSetOrgUnit={handleSetOrgUnit} previousOrgUnitId={previousOrgUnitId} - selectedOrgUnit={selectedOrgUnit} + selectedOrgUnit={{ ...selectedOrgUnit, name: ouNameError ? undefined : selectedOrgUnit.name }} selectedOrgUnitId={selectedOrgUnitId} selectedProgramId={selectedProgramId} selectedCategories={selectedCategories} diff --git a/src/core_modules/capture-core/reducers/descriptions/activePage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/activePage.reducerDescription.js index 935a25a30f..0a72c610a6 100644 --- a/src/core_modules/capture-core/reducers/descriptions/activePage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/activePage.reducerDescription.js @@ -81,18 +81,14 @@ export const activePageDesc = createReducerDescription({ lockedSelectorLoads: false, }), - [enrollmentPageActionTypes.INFORMATION_FETCH]: state => ({ - ...state, - lockedSelectorLoads: true, - }), - [enrollmentPageActionTypes.INFORMATION_SUCCESS_FETCH]: state => ({ + [enrollmentPageActionTypes.ERROR_VIEW]: (state, { payload: { error } }) => ({ ...state, lockedSelectorLoads: false, + selectionsError: { error }, }), - [enrollmentPageActionTypes.INFORMATION_ERROR_FETCH]: (state, { payload: { error } }) => ({ + [enrollmentPageActionTypes.CLEAR_ERROR_VIEW]: state => ({ ...state, - lockedSelectorLoads: false, - selectionsError: { error }, + selectionsError: null, }), }, 'activePage', { selectionsError: null, diff --git a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js index 77d92128f3..47626d1f06 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js @@ -4,76 +4,188 @@ import { enrollmentPageActionTypes } from '../../components/Pages/Enrollment/Enr import { enrollmentPageStatuses, enrollmentAccessLevels, + selectionStatus, } from '../../components/Pages/Enrollment/EnrollmentPage.constants'; const initialReducerValue = { + pageOpen: true, enrollmentPageStatus: null, + enrollmentAccessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, + fetchStatus: { + enrollmentId: selectionStatus.READY, + programId: selectionStatus.READY, + teiId: selectionStatus.READY, + }, }; const { - INFORMATION_LOADING_FETCH, - INFORMATION_ERROR_FETCH, - INFORMATION_SUCCESS_FETCH, - ENROLLMENTS_ERROR_FETCH, - ENROLLMENTS_SUCCESS_FETCH, - PAGE_CLEAN, + PAGE_OPEN, + PAGE_CLOSE, + RESET_ENROLLMENT_ID, + FETCH_ENROLLMENT_ID, + FETCH_ENROLLMENT_ID_SUCCESS, + FETCH_ENROLLMENT_ID_ERROR, + FETCH_TEI, + FETCH_TEI_SUCCESS, + FETCH_TEI_ERROR, + COMMIT_TRACKER_PROGRAM_ID, + COMMIT_NON_TRACKER_PROGRAM_ID, + PROGRAM_ID_ERROR, + FETCH_ENROLLMENTS, + FETCH_ENROLLMENTS_ERROR, + FETCH_ENROLLMENTS_SUCCESS, DEFAULT_VIEW, + LOADING_VIEW, MISSING_MESSAGE_VIEW, + ERROR_VIEW, DELETE_ENROLLMENT, UPDATE_TEI_DISPLAY_NAME, UPDATE_ENROLLMENT_DATE, } = enrollmentPageActionTypes; export const enrollmentPageDesc = createReducerDescription({ - [INFORMATION_LOADING_FETCH]: state => ({ - ...state, - enrollmentPageStatus: enrollmentPageStatuses.LOADING, - }), - [INFORMATION_ERROR_FETCH]: state => ({ - ...state, - enrollmentPageStatus: enrollmentPageStatuses.ERROR, - }), - [INFORMATION_SUCCESS_FETCH]: - (state, { payload: - { - teiDisplayName, + [PAGE_OPEN]: + () => initialReducerValue, + [PAGE_CLOSE]: + state => ({ + ...state, + pageOpen: false, + }), + [RESET_ENROLLMENT_ID]: + ({ enrollmentId, ...state }) => ({ + ...state, + fetchStatus: { + ...state.fetchStatus, + enrollmentId: selectionStatus.READY, + }, + }), + [FETCH_ENROLLMENT_ID]: + ({ programId, teiId, enrollments, ...state }, { payload: { enrollmentId } }) => ({ + ...state, + enrollmentId, + fetchStatus: { + ...state.fetchStatus, + enrollmentId: selectionStatus.LOADING, + }, + enrollmentPageStatus: enrollmentPageStatuses.LOADING, + }), + [FETCH_ENROLLMENT_ID_SUCCESS]: + state => ({ + ...state, + fetchStatus: { + ...state.fetchStatus, + enrollmentId: selectionStatus.READY, + }, + }), + [FETCH_ENROLLMENT_ID_ERROR]: + state => ({ + ...state, + fetchStatus: { + ...state.fetchStatus, + enrollmentId: selectionStatus.ERROR, + }, + }), + [FETCH_TEI]: + ({ enrollments, ...state }, { payload: { teiId } }) => ({ + ...state, + teiId, + fetchStatus: { + ...state.fetchStatus, + teiId: selectionStatus.LOADING, + }, + enrollmentPageStatus: enrollmentPageStatuses.LOADING, + enrollmentAccessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, + }), + [FETCH_TEI_SUCCESS]: + (state, { payload: { tetId, teiDisplayName } }) => ({ + ...state, tetId, - }, - }) => ({ - ...state, - enrollmentPageStatus: enrollmentPageStatuses.DEFAULT, - teiDisplayName, - tetId, - }), - [ENROLLMENTS_ERROR_FETCH]: - ({ enrollments, ...state }, { payload: { accessLevel, programId } }) => ({ - ...state, - programId, - enrollmentPageStatus: enrollmentPageStatuses.MISSING_SELECTIONS, - enrollmentAccessLevel: accessLevel, - }), - [ENROLLMENTS_SUCCESS_FETCH]: - (state, { payload: { enrollments, programId } }) => ({ - ...state, - programId, - enrollments, - enrollmentAccessLevel: enrollmentAccessLevels.FULL_ACCESS, - }), + teiDisplayName, + fetchStatus: { + ...state.fetchStatus, + teiId: selectionStatus.READY, + }, + }), + [FETCH_TEI_ERROR]: + state => ({ + ...state, + fetchStatus: { + ...state.fetchStatus, + teiId: selectionStatus.ERROR, + }, + }), + [COMMIT_TRACKER_PROGRAM_ID]: + ({ enrollments, ...state }, { payload: { programId } }) => ({ + ...state, + programId, + fetchStatus: { + ...state.fetchStatus, + programId: selectionStatus.READY, + }, + enrollmentAccessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, + }), + [COMMIT_NON_TRACKER_PROGRAM_ID]: + ({ enrollments, ...state }, { payload: { programId } }) => ({ + ...state, + programId, + fetchStatus: { + ...state.fetchStatus, + programId: selectionStatus.ERROR, + }, + enrollmentAccessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, + }), + [PROGRAM_ID_ERROR]: + (state, { payload: { programId } }) => ({ + ...state, + programId, + fetchStatus: { + ...state.fetchStatus, + programId: selectionStatus.ERROR, + }, + }), + [FETCH_ENROLLMENTS]: + ({ enrollments, ...state }) => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.LOADING, + enrollmentAccessLevel: enrollmentAccessLevels.UNKNOWN_ACCESS, + }), + [FETCH_ENROLLMENTS_SUCCESS]: + (state, { payload: { enrollments } }) => ({ + ...state, + enrollments, + enrollmentPageStatus: enrollmentPageStatuses.DEFAULT, + enrollmentAccessLevel: enrollmentAccessLevels.FULL_ACCESS, + }), + [FETCH_ENROLLMENTS_ERROR]: + ({ enrollments, ...state }, { payload: { accessLevel } }) => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.MISSING_SELECTIONS, + enrollmentAccessLevel: accessLevel, + }), [DEFAULT_VIEW]: - state => ({ - ...state, - enrollmentPageStatus: enrollmentPageStatuses.DEFAULT, - }), + state => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.DEFAULT, + }), + [LOADING_VIEW]: + state => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.LOADING, + }), [MISSING_MESSAGE_VIEW]: - state => ({ - ...state, - enrollmentPageStatus: enrollmentPageStatuses.MISSING_SELECTIONS, - }), + state => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.MISSING_SELECTIONS, + }), + [ERROR_VIEW]: + state => ({ + ...state, + enrollmentPageStatus: enrollmentPageStatuses.ERROR, + }), [UPDATE_TEI_DISPLAY_NAME]: - (state, { payload: { teiDisplayName }, - }) => ({ - ...state, - teiDisplayName, - }), + (state, { payload: { teiDisplayName } }) => ({ + ...state, + teiDisplayName, + }), [UPDATE_ENROLLMENT_DATE]: (state, { payload: { enrollmentId, enrollmentDate } }) => ({ ...state, @@ -84,7 +196,6 @@ export const enrollmentPageDesc = createReducerDescription({ return enrollment; }), }), - [PAGE_CLEAN]: () => initialReducerValue, [DELETE_ENROLLMENT]: (state, { payload: { enrollmentId } }) => ({ ...state, enrollments: [ diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index fb5aec0812..6e8c02694f 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -187,11 +187,22 @@ import { } from '../core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics'; import { - fetchEnrollmentPageInformationFromUrlEpic, - startFetchingTeiFromEnrollmentIdEpic, - startFetchingTeiFromTeiIdEpic, + changedEnrollmentIdEpic, + fetchEnrollmentIdEpic, + verifyEnrollmentIdSuccessEpic, + enrollmentIdErrorEpic, + changedTeiIdEpic, + resetTeiIdEpic, + fetchTeiIdEpic, + verifyTeiFetchSuccessEpic, + fetchTeiErrorEpic, + changedProgramIdEpic, + programIdErrorEpic, + teiOrProgramChangeEpic, fetchEnrollmentsEpic, - openEnrollmentPageEpic, + verifyFetchedEnrollmentsEpic, + autoSwitchOrgUnitEpic, + clearErrorViewEpic, } from '../core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics'; import { saveNewEventSucceededEpic, @@ -326,11 +337,22 @@ export const epics = combineEpics( startSavingNewTrackedEntityInstanceWithEnrollmentEpic, completeSavingNewTrackedEntityInstanceEpic, completeSavingNewTrackedEntityInstanceWithEnrollmentEpic, - fetchEnrollmentPageInformationFromUrlEpic, - startFetchingTeiFromEnrollmentIdEpic, - startFetchingTeiFromTeiIdEpic, + changedEnrollmentIdEpic, + fetchEnrollmentIdEpic, + verifyEnrollmentIdSuccessEpic, + enrollmentIdErrorEpic, + changedTeiIdEpic, + resetTeiIdEpic, + fetchTeiIdEpic, + verifyTeiFetchSuccessEpic, + fetchTeiErrorEpic, + changedProgramIdEpic, + programIdErrorEpic, + teiOrProgramChangeEpic, fetchEnrollmentsEpic, - openEnrollmentPageEpic, + verifyFetchedEnrollmentsEpic, + autoSwitchOrgUnitEpic, + clearErrorViewEpic, saveNewEventStageEpic, saveNewEventStageFailedEpic, saveNewEventInStageLocationChangeEpic,