From 189c57721e45bc639bca9580c05b03d97f2cb85b Mon Sep 17 00:00:00 2001 From: Tony Valle <79843014+superskip@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:59:37 +0200 Subject: [PATCH] feat: [DHIS2-14334] edit enrollment date (#3350) --- .../WidgetEnrollment/index.js | 12 +- i18n/en.pot | 3 + package.json | 1 + .../Enrollment/EnrollmentPage.actions.js | 4 + .../EnrollmentPageDefault.component.js | 4 + .../EnrollmentPageDefault.container.js | 20 ++- .../EnrollmentPageDefault.types.js | 2 + ...EnrollmentAddEventPageDefault.component.js | 1 + .../EnrollmentEditEventPage.component.js | 1 + .../enrollment.actions.js | 12 ++ .../common/EnrollmentOverviewDomain/index.js | 2 + .../Actions/Actions.container.js | 8 +- .../WidgetEnrollment/Date/Date.component.js | 151 ++++++++++++++++++ .../components/WidgetEnrollment/Date/index.js | 2 + .../WidgetEnrollment.component.js | 52 +++--- .../WidgetEnrollment.container.js | 33 +++- .../WidgetEnrollment/enrollment.types.js | 9 +- .../WidgetEnrollment/hooks/useEnrollment.js | 49 +++++- .../WidgetEnrollment/hooks/useProgram.js | 2 +- .../hooks/useUpdateEnrollment.js | 42 +++++ .../WidgetEnrollment/processErrorReports.js | 8 + .../enrollmentDomain.reducerDescription.js | 16 ++ .../enrollmentPage.reducerDescription.js | 11 ++ yarn.lock | 2 +- 24 files changed, 400 insertions(+), 47 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js create mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js create mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js create mode 100644 src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js b/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js index 26146624d7..4584e1ba2a 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js +++ b/cypress/integration/WidgetsForEnrollmentPages/WidgetEnrollment/index.js @@ -19,18 +19,18 @@ Then('the enrollment widget should be opened', () => { }); Then('the user sees the enrollment date', () => { - cy.get('[data-test="widget-enrollment"]').within(() => { + cy.get('[data-test="widget-enrollment-enrollment-date"]').within(() => { cy.get('[data-test="widget-enrollment-icon-calendar"]').should('exist'); - cy.get('[data-test="widget-enrollment-enrollment-date"]') - .contains(`Date of enrollment ${getCurrentYear()}-08-01`) + cy.get('[data-test="widget-enrollment-date"]') + .contains(`Date of enrollment: ${getCurrentYear()}-08-01`) .should('exist'); }); }); Then('the user sees the incident date', () => { - cy.get('[data-test="widget-enrollment"]').within(() => { - cy.get('[data-test="widget-enrollment-incident-date"]') - .contains(`Date of birth ${getCurrentYear()}-08-01`) + cy.get('[data-test="widget-enrollment-incident-date"]').within(() => { + cy.get('[data-test="widget-enrollment-date"]') + .contains(`Date of birth: ${getCurrentYear()}-08-01`) .should('exist'); }); }); diff --git a/i18n/en.pot b/i18n/en.pot index 52dcc1b13e..9674ac525a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -1158,6 +1158,9 @@ msgstr "Remove mark for follow-up" msgid "Mark for follow-up" msgstr "Mark for follow-up" +msgid "Existing dates for auto-generated events will not be updated." +msgstr "Existing dates for auto-generated events will not be updated." + msgid "Enrollment date" msgstr "Enrollment date" diff --git a/package.json b/package.json index 6f9a6db16c..b2ed536c77 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ }, "resolutions": { "@babel/preset-react": "7.16.7", + "@js-temporal/polyfill": "0.4.3", "core-js": "2.5.7" }, "browserslist": { 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 a1f8c3250a..5f29c4daac 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 @@ -22,6 +22,7 @@ export const enrollmentPageActionTypes = { DELETE_ENROLLMENT: 'EnrollmentPage.DeleteEnrollment', UPDATE_TEI_DISPLAY_NAME: 'EnrollmentPage.UpdateTeiDisplayName', + UPDATE_ENROLLMENT_DATE: 'EnrollmentPage.UpdateEnrollmentDate', }; export const fetchEnrollmentPageInformation = () => @@ -73,3 +74,6 @@ export const updateTeiDisplayName = (teiDisplayName: string) => actionCreator(enrollmentPageActionTypes.UPDATE_TEI_DISPLAY_NAME)({ teiDisplayName, }); + +export const updateEnrollmentDate = ({ enrollmentId, enrollmentDate }: { enrollmentId: string, enrollmentDate: string }) => + actionCreator(enrollmentPageActionTypes.UPDATE_ENROLLMENT_DATE)({ enrollmentId, enrollmentDate }); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index d2b5b6f091..dac5ca2c8a 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -60,6 +60,8 @@ export const EnrollmentPageDefaultPlain = ({ classes, onEventClick, onUpdateTeiAttributeValues, + onUpdateEnrollmentDate, + onUpdateIncidentDate, onEnrollmentError, }: PlainProps) => ( <> @@ -108,6 +110,8 @@ export const EnrollmentPageDefaultPlain = ({ programId={program.id} onDelete={onDelete} onAddNew={onAddNew} + onUpdateEnrollmentDate={onUpdateEnrollmentDate} + onUpdateIncidentDate={onUpdateIncidentDate} onError={onEnrollmentError} />} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 976e03492a..12eb12de18 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -8,8 +8,15 @@ import { useHistory } from 'react-router-dom'; import { useCommonEnrollmentDomainData, updateEnrollmentAttributeValues, + updateEnrollmentDate, + updateIncidentDate, showEnrollmentError, } from '../../common/EnrollmentOverviewDomain'; +import { + updateEnrollmentDate as updateTopBarEnrollmentDate, + deleteEnrollment, + updateTeiDisplayName, +} from '../EnrollmentPage.actions'; import { useTrackerProgram } from '../../../../hooks/useTrackerProgram'; import { useRulesEngineOrgUnit } from '../../../../hooks/useRulesEngineOrgUnit'; import { EnrollmentPageDefaultComponent } from './EnrollmentPageDefault.component'; @@ -20,7 +27,6 @@ import { useRuleEffects, } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; -import { deleteEnrollment, updateTeiDisplayName } from '../EnrollmentPage.actions'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; export const EnrollmentPageDefault = () => { @@ -74,6 +80,7 @@ export const EnrollmentPageDefault = () => { const onEventClick = (eventId: string) => { history.push(`/enrollmentEventEdit?${buildUrlQueryString({ orgUnitId, eventId })}`); }; + const onUpdateTeiAttributeValues = useCallback((updatedAttributeValues, teiDisplayName) => { dispatch(updateEnrollmentAttributeValues(updatedAttributeValues .map(({ attribute, value }) => ({ id: attribute, value })), @@ -81,6 +88,15 @@ export const EnrollmentPageDefault = () => { dispatch(updateTeiDisplayName(teiDisplayName)); }, [dispatch]); + const onUpdateEnrollmentDate = useCallback((enrollmentDate) => { + dispatch(updateEnrollmentDate(enrollmentDate)); + dispatch(updateTopBarEnrollmentDate({ enrollmentId, enrollmentDate })); + }, [dispatch, enrollmentId]); + + const onUpdateIncidentDate = useCallback((incidentDate) => { + dispatch(updateIncidentDate(incidentDate)); + }, [dispatch]); + const onAddNew = () => { history.push(`/new?${buildUrlQueryString({ orgUnitId, programId, teiId })}`); }; @@ -107,6 +123,8 @@ export const EnrollmentPageDefault = () => { hideWidgets={hideWidgets} onEventClick={onEventClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} + onUpdateEnrollmentDate={onUpdateEnrollmentDate} + onUpdateIncidentDate={onUpdateIncidentDate} onEnrollmentError={onEnrollmentError} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 3822b286a0..b8e3dbd4dd 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -19,6 +19,8 @@ export type Props = {| onCreateNew: (stageId: string) => void, onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, + onUpdateEnrollmentDate: (enrollmentDate: string) => void, + onUpdateIncidentDate: (incidentDate: string) => void, onEnrollmentError: (message: string) => void, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js index e741014b20..f7abac3990 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js @@ -126,6 +126,7 @@ const EnrollmentAddEventPagePain = ({ teiId={teiId} enrollmentId={enrollmentId} programId={programId} + readOnlyMode onDelete={onDelete} onAddNew={onAddNew} onError={onEnrollmentError} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index b5efd930b9..1add434b21 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -138,6 +138,7 @@ const EnrollmentEditEventPagePain = ({ teiId={teiId} enrollmentId={enrollmentId} programId={programId} + readOnlyMode onDelete={onDelete} onAddNew={onAddNew} onError={onEnrollmentError} diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js index 6cdbe894ba..beb3832f89 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.js @@ -4,6 +4,8 @@ import { actionCreator } from '../../../../actions/actions.utils'; export const enrollmentSiteActionTypes = { COMMON_ENROLLMENT_SITE_DATA_SET: 'EnrollmentSite.SetCommonData', + UPDATE_ENROLLMENT_DATE: 'Enrollment.UpdateEnrollmentDate', + UPDATE_INCIDENT_DATE: 'Enrollment.UpdateIncidentDate', UPDATE_ENROLLMENT_EVENTS: 'Enrollment.UpdateEnrollmentEvents', UPDATE_ENROLLMENT_EVENTS_WITHOUT_ID: 'Enrollment.UpdateEnrollmentEventsWithoutId', UPDATE_ENROLLMENT_ATTRIBUTE_VALUES: 'Enrollment.UpdateEnrollmentAttributeValues', @@ -18,6 +20,16 @@ export const enrollmentSiteActionTypes = { export const setCommonEnrollmentSiteData = (enrollment: ApiEnrollment, attributeValues: ApiAttributeValues) => actionCreator(enrollmentSiteActionTypes.COMMON_ENROLLMENT_SITE_DATA_SET)({ enrollment, attributeValues }); +export const updateEnrollmentDate = (enrollmentDate: string) => + actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_DATE)({ + enrollmentDate, + }); + +export const updateIncidentDate = (incidentDate: string) => + actionCreator(enrollmentSiteActionTypes.UPDATE_INCIDENT_DATE)({ + incidentDate, + }); + export const updateEnrollmentEvents = (eventId: string, eventData: Object) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_EVENTS)({ eventId, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js index e7a9b66f5b..f79612916d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/index.js @@ -2,6 +2,8 @@ export type { HideWidgets, WidgetEffects } from './enrollmentOverviewDomain.types'; export { enrollmentSiteActionTypes, + updateEnrollmentDate, + updateIncidentDate, updateEnrollmentEvents, commitEnrollmentEvent, rollbackEnrollmentEvent, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js index e83fdfda17..2ce98fcb38 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Actions.container.js @@ -3,6 +3,7 @@ import { useDataMutation } from '@dhis2/app-runtime'; import React from 'react'; import { ActionsComponent } from './Actions.component'; import type { Props } from './actions.types'; +import { processErrorReports } from '../processErrorReports'; const enrollmentUpdate = { resource: 'tracker?async=false&importStrategy=UPDATE', @@ -18,13 +19,6 @@ const enrollmentDelete = { enrollments: [enrollment], }), }; -const processErrorReports = (error) => { - // $FlowFixMe[prop-missing] - const errorReports = error?.details?.validationReport?.errorReports; - return errorReports?.length > 0 - ? errorReports.reduce((acc, errorReport) => `${acc} ${errorReport.message}`, '') - : error.message; -}; export const Actions = ({ enrollment = {}, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js new file mode 100644 index 0000000000..f3c7f36617 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.js @@ -0,0 +1,151 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import moment from 'moment'; +import { + Button, + CalendarInput, + IconCalendar16, + IconEdit16, + colors, + spacersNum, +} from '@dhis2/ui'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core'; +import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; +import { dataElementTypes } from '../../../metaData'; + +type Props = { + date: string, + dateLabel: string, + editEnabled: boolean, + displayAutoGeneratedEventWarning: boolean, + onSave: (string) => void, + ...CssClasses, +} + +const styles = { + editButton: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + cursor: 'pointer', + border: 'none', + borderRadius: '3px', + background: 'transparent', + color: colors.grey600, + padding: 0, + marginLeft: '2px', + '&:focus': { + outline: 'none', + background: colors.grey200, + color: colors.grey800, + }, + '&:hover': { + background: colors.grey200, + color: colors.grey800, + }, + }, + calendar: { + paddingTop: '6px', + }, + inputField: { + maxWidth: '200px', + }, + buttonStrip: { + display: 'flex', + gap: `${spacersNum.dp4}px`, + margin: `${spacersNum.dp4}px 0`, + }, + note: { + fontSize: '12px', + color: colors.grey700, + }, +}; + +const DateComponentPlain = ({ + date, + dateLabel, + editEnabled, + displayAutoGeneratedEventWarning, + onSave, + classes, +}: Props) => { + const [editMode, setEditMode] = useState(false); + const [selectedDate, setSelectedDate] = useState(); + const dateChangeHandler = useCallback(({ calendarDateString }) => { + setSelectedDate(calendarDateString); + }, [setSelectedDate]); + const displayDate = String(convertValueClientToView(date, dataElementTypes.DATE)); + + const onOpenEdit = () => { + // CalendarInput component only supports the YYYY-MM-DD format + setSelectedDate(moment(date).format('YYYY-MM-DD')); + setEditMode(true); + }; + const saveHandler = () => { + // CalendarInput component only supports the YYYY-MM-DD format + if (selectedDate) { + const newDate = moment.utc(selectedDate, 'YYYY-MM-DD').format('YYYY-MM-DDTHH:mm:ss.SSS'); + if (newDate !== date) { + onSave(newDate); + } + } + setEditMode(false); + }; + + return editMode ? ( +
+
+ +
+
+ + +
+ {displayAutoGeneratedEventWarning && ( +
+ {i18n.t('Existing dates for auto-generated events will not be updated.')} +
+ )} +
+ ) : ( +
+ + + + {dateLabel}{': '} + {displayDate} + {editEnabled && + + } +
+ ); +}; + +export const Date = withStyles(styles)(DateComponentPlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js b/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js new file mode 100644 index 0000000000..5fc124daa8 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/index.js @@ -0,0 +1,2 @@ +// @flow +export { Date } from './Date.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js index 977793676c..15ebdb7c41 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.js @@ -4,7 +4,6 @@ import moment from 'moment'; import { IconClock16, IconDimensionOrgUnit16, - IconCalendar16, IconLocation16, colors, Tag, @@ -20,6 +19,7 @@ import { Status } from './Status'; import { convertValue as convertValueServerToClient } from '../../converters/serverToClient'; import { convertValue as convertValueClientToView } from '../../converters/clientToView'; import { dataElementTypes } from '../../metaData'; +import { Date } from './Date'; import { Actions } from './Actions'; const styles = { @@ -52,11 +52,15 @@ export const WidgetEnrollmentPlain = ({ ownerOrgUnit = {}, refetchEnrollment, refetchTEI, - error, + initError, loading, canAddNew, + editDateEnabled, + displayAutoGeneratedEventWarning, onDelete, onAddNew, + updateEnrollmentDate, + updateIncidentDate, onError, onSuccess, }: PlainProps) => { @@ -72,13 +76,13 @@ export const WidgetEnrollmentPlain = ({ onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - {error && ( + {initError && (
{i18n.t('Enrollment widget could not be loaded. Please try again later')}
)} {loading && } - {!error && !loading && ( + {!initError && !loading && (
{enrollment.followUp && ( @@ -89,28 +93,28 @@ export const WidgetEnrollmentPlain = ({
-
- - - - {getEnrollmentDateLabel(program)}{' '} - {convertValueClientToView( - convertValueServerToClient(enrollment.enrolledAt, dataElementTypes.DATE), - dataElementTypes.DATE, - )} -
+ + + {program.displayIncidentDate && ( -
- - - - {getIncidentDateLabel(program)}{' '} - {convertValueClientToView( - convertValueServerToClient(enrollment.occurredAt, dataElementTypes.DATE), - dataElementTypes.DATE, - )} -
+ + + )}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js index 8660f704f4..38296cfa32 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.container.js @@ -10,8 +10,30 @@ import { useProgram } from './hooks/useProgram'; import type { Props } from './enrollment.types'; import { plainStatus } from './constants/status.const'; -export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onAddNew, onError, onSuccess }: Props) => { - const { error: errorEnrollment, enrollment, refetch: refetchEnrollment } = useEnrollment(enrollmentId); +export const WidgetEnrollment = ({ + teiId, + enrollmentId, + programId, + readOnlyMode = false, + onDelete, + onAddNew, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, + onSuccess, +}: Props) => { + const { + enrollment, + updateEnrollmentDate, + updateIncidentDate, + error: errorEnrollment, + refetch: refetchEnrollment, + } = useEnrollment({ + enrollmentId, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, + }); const { error: errorProgram, program } = useProgram(programId); const { error: errorOwnerOrgUnit, @@ -23,6 +45,7 @@ export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onA const canAddNew = enrollments .filter(item => item.program === programId) .every(item => item.status !== plainStatus.ACTIVE); + const containsAutoGeneratedEvent = program && program.programStages.some(({ autoGenerateEvent }) => autoGenerateEvent); const error = errorEnrollment || errorProgram || errorOwnerOrgUnit || errorOrgUnit; if (error) { @@ -33,6 +56,8 @@ export const WidgetEnrollment = ({ teiId, enrollmentId, programId, onDelete, onA diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js index a3eacd0fdb..d4e14794b8 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/enrollment.types.js @@ -5,8 +5,11 @@ export type Props = {| teiId: string, enrollmentId: string, programId: string, + readOnlyMode?: boolean, onDelete: () => void, onAddNew: () => void, + onUpdateEnrollmentDate?: (enrollmentDate: string) => void, + onUpdateIncidentDate?: (enrollmentDate: string) => void, onError?: (message: string) => void, onSuccess?: () => void, |}; @@ -17,9 +20,13 @@ export type PlainProps = {| ownerOrgUnit: Object, refetchEnrollment: QueryRefetchFunction, refetchTEI: QueryRefetchFunction, - error?: FetchError, + initError?: FetchError, loading: boolean, canAddNew: boolean, + editDateEnabled: boolean, + displayAutoGeneratedEventWarning: boolean, + updateEnrollmentDate: (enrollmentDate: string) => void, + updateIncidentDate: (incidentDate: string) => void, onDelete: () => void, onAddNew: () => void, onError?: (message: string) => void, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js index 87ae5120c4..e9b2d305fd 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useEnrollment.js @@ -1,8 +1,23 @@ // @flow -import { useMemo, useEffect } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; +import { useUpdateEnrollment } from './useUpdateEnrollment'; + +type Props = { + enrollmentId: string, + onUpdateEnrollmentDate?: (date: string) => void, + onUpdateIncidentDate?: (date: string) => void, + onError?: (error: any) => void, +} + +export const useEnrollment = ({ + enrollmentId, + onUpdateEnrollmentDate, + onUpdateIncidentDate, + onError, +}: Props) => { + const [enrollment, setEnrollment] = useState(); -export const useEnrollment = (enrollmentId: string) => { const { error, loading, data, refetch } = useDataQuery( useMemo( () => ({ @@ -20,5 +35,33 @@ export const useEnrollment = (enrollmentId: string) => { enrollmentId && refetch({ variables: { enrollmentId } }); }, [refetch, enrollmentId]); - return { error, refetch, enrollment: !loading && data?.enrollment }; + useEffect(() => { + if (data) { + setEnrollment(data.enrollment); + } + }, [setEnrollment, data]); + + const updateEnrollmentDate = useUpdateEnrollment({ + enrollment, + setEnrollment, + propertyName: 'enrolledAt', + updateHandler: onUpdateEnrollmentDate, + onError, + }); + + const updateIncidentDate = useUpdateEnrollment({ + enrollment, + setEnrollment, + propertyName: 'occurredAt', + updateHandler: onUpdateIncidentDate, + onError, + }); + + return { + error, + refetch, + enrollment: !loading && enrollment, + updateEnrollmentDate, + updateIncidentDate, + }; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js index 937f5644c6..fec7939f6d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.js @@ -10,7 +10,7 @@ export const useProgram = (programId: string) => { resource: `programs/${programId}`, params: { fields: [ - 'displayIncidentDate,incidentDateLabel,enrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName]', + 'displayIncidentDate,incidentDateLabel,enrollmentDateLabel,onlyEnrollOnce,trackedEntityType[displayName],programStages[autoGenerateEvent],access', ], }, }, diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js new file mode 100644 index 0000000000..fd7c96f827 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useUpdateEnrollment.js @@ -0,0 +1,42 @@ +// @flow +import { useCallback } from 'react'; +import { useDataMutation } from '@dhis2/app-runtime'; +import { processErrorReports } from '../processErrorReports'; + +const enrollmentUpdate = { + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: enrollment => ({ + enrollments: [enrollment], + }), +}; + +export const useUpdateEnrollment = ({ + enrollment, + setEnrollment, + propertyName, + updateHandler, + onError, +}: { + enrollment: any, + setEnrollment: (enrollment: any) => void, + propertyName: string, + updateHandler?: (value: any) => void, + onError?: (error: any) => void, +}) => { + const [updateEnrollmentMutation] = useDataMutation(enrollmentUpdate, { + onError: (e) => { + setEnrollment(enrollment); + updateHandler && updateHandler(enrollment[propertyName]); + onError && onError(processErrorReports(e)); + }, + }); + + return useCallback((value: string) => { + const updatedEnrollment = { ...enrollment }; + updatedEnrollment[propertyName] = value; + setEnrollment(updatedEnrollment); + updateEnrollmentMutation(updatedEnrollment); + updateHandler && updateHandler(value); + }, [enrollment, setEnrollment, propertyName, updateHandler, updateEnrollmentMutation]); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js b/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js new file mode 100644 index 0000000000..d60fa40b31 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEnrollment/processErrorReports.js @@ -0,0 +1,8 @@ +// @flow +export const processErrorReports = (error: any) => { + // $FlowFixMe[prop-missing] + const errorReports = error?.details?.validationReport?.errorReports; + return errorReports?.length > 0 + ? errorReports.reduce((acc, errorReport) => `${acc} ${errorReport.message}`, '') + : error.message; +}; 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 b3470cbe24..9e0745fa5b 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -8,6 +8,8 @@ import { actionTypes as editEventActionTypes } from '../../components/WidgetEven const initialReducerValue = {}; const { COMMON_ENROLLMENT_SITE_DATA_SET, + UPDATE_ENROLLMENT_DATE, + UPDATE_INCIDENT_DATE, UPDATE_ENROLLMENT_EVENTS, UPDATE_ENROLLMENT_EVENTS_WITHOUT_ID, UPDATE_ENROLLMENT_ATTRIBUTE_VALUES, @@ -25,6 +27,20 @@ export const enrollmentDomainDesc = createReducerDescription( attributeValues, enrollmentId: enrollment?.enrollment, }), + [UPDATE_ENROLLMENT_DATE]: (state, { payload: { enrollmentDate } }) => ({ + ...state, + enrollment: { + ...state.enrollment, + enrolledAt: enrollmentDate, + }, + }), + [UPDATE_INCIDENT_DATE]: (state, { payload: { incidentDate } }) => ({ + ...state, + enrollment: { + ...state.enrollment, + occurredAt: incidentDate, + }, + }), [UPDATE_ENROLLMENT_EVENTS]: ( state, { payload: { eventId, eventData } }, 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 bfff16fabb..87827640b1 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentPage.reducerDescription.js @@ -20,6 +20,7 @@ const { MISSING_MESSAGE_VIEW, DELETE_ENROLLMENT, UPDATE_TEI_DISPLAY_NAME, + UPDATE_ENROLLMENT_DATE, } = enrollmentPageActionTypes; export const enrollmentPageDesc = createReducerDescription({ @@ -73,6 +74,16 @@ export const enrollmentPageDesc = createReducerDescription({ ...state, teiDisplayName, }), + [UPDATE_ENROLLMENT_DATE]: + (state, { payload: { enrollmentId, enrollmentDate } }) => ({ + ...state, + enrollments: state.enrollments.map((enrollment) => { + if (enrollment.enrollment === enrollmentId) { + enrollment.enrolledAt = enrollmentDate; + } + return enrollment; + }), + }), [PAGE_CLEAN]: () => initialReducerValue, [DELETE_ENROLLMENT]: (state, { payload: { enrollmentId } }) => ({ ...state, diff --git a/yarn.lock b/yarn.lock index 0c0d33e902..983b457b91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2948,7 +2948,7 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@js-temporal/polyfill@^0.4.2": +"@js-temporal/polyfill@0.4.3", "@js-temporal/polyfill@^0.4.2": version "0.4.3" resolved "https://registry.yarnpkg.com/@js-temporal/polyfill/-/polyfill-0.4.3.tgz#e8f8cf86745eb5050679c46a5ebedb9a9cc1f09b" integrity sha512-6Fmjo/HlkyVCmJzAPnvtEWlcbQUSRhi8qlN9EtJA/wP7FqXsevLLrlojR44kzNzrRkpf7eDJ+z7b4xQD/Ycypw==