From d61efae8b24ffb34a84e765bc397de87ce76c125 Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Thu, 25 Jan 2024 12:45:22 +0100 Subject: [PATCH] feat: [DHIS2-15480] widget assignee (#3412) --- .../WidgetAssignee/index.js | 52 +++++ .../WidgetsForEnrollmentEditEvent.feature | 11 +- .../WidgetsForEnrollmentEditEvent/index.js | 2 + i18n/en.pot | 52 +++-- .../epics/getConvertedNewSingleEvent.js | 2 +- .../components/FormFields/UserField/index.js | 1 + .../NewEventWorkspace.component.js | 1 + .../EnrollmentEditEventPage.actions.js | 15 ++ .../EnrollmentEditEventPage.component.js | 213 +++++++++++++----- .../EnrollmentEditEventPage.container.js | 38 +++- .../EnrollmentEditEventPage.types.js | 12 +- .../Pages/EnrollmentEditEvent/hooks/index.js | 1 + .../hooks/useAssignedUserSaveContext.js | 11 + .../Pages/EnrollmentEditEvent/index.js | 3 + .../AssigneeSection.component.js | 52 ----- .../AssigneeSection.container.js | 54 +++-- .../AssigneeSection/Contents.component.js | 40 ---- .../AssigneeSection/DisplayMode.component.js | 90 -------- .../AssigneeSection/EditMode.component.js | 61 ----- .../assigneeSection.actions.js | 27 --- .../RightColumn/AssigneeSection/index.js | 2 - .../AssigneeSection/saveAssignee.epic.js | 37 --- .../RightColumnWrapper.component.js | 2 +- .../ViewEventComponent/ViewEvent.component.js | 22 +- .../ViewEventComponent/ViewEvent.container.js | 37 ++- .../ViewEventComponent/viewEvent.actions.js | 10 + .../ViewEventComponent/viewEvent.selectors.js | 24 +- .../components/Pages/ViewEvent/index.js | 2 - .../useCommonEnrollmentDomainData.types.js | 4 +- .../useRuleEffects/useRuleEffects.js | 2 +- .../WidgetAssignee/DisplayMode.component.js | 84 +++++++ .../WidgetAssignee/EditMode.component.js | 63 ++++++ .../WidgetAssignee.component.js | 62 +++++ .../WidgetAssignee.container.js | 50 ++++ .../WidgetAssignee/WidgetAssignee.types.js | 26 +++ .../components/WidgetAssignee/converter.js | 14 ++ .../components/WidgetAssignee/hooks/index.js | 2 + .../WidgetAssignee/hooks/useUserAvatar.js | 20 ++ .../components/WidgetAssignee/index.js | 3 + .../Validated/getConvertedAddEvent.js | 2 +- .../EditEventDataEntry.component.js | 4 + .../WidgetEventEdit.container.js | 2 + .../WidgetEventEdit/widgetEventEdit.types.js | 2 + .../Assignee/Assignee.types.js | 1 + .../WidgetEventSchedule.actions.js | 2 +- .../WidgetEventSchedule.container.js | 17 +- .../widgetEventSchedule.types.js | 6 +- .../DataEntry/hooks/useEvents.js | 2 +- .../types/common.types.js | 4 +- .../capture-core/converters/clientToServer.js | 22 +- .../capture-core/converters/index.js | 2 +- .../capture-core/converters/serverToClient.js | 2 +- .../capture-core/events/eventRequests.js | 2 +- .../mainEventClientToServerConverter.js | 6 +- .../events/prepareEnrollmentEvents.js | 2 +- .../capture-core/flow/apiTypes.js | 6 +- .../enrollmentDomain.reducerDescription.js | 14 ++ .../feedback.reducerDescriptionGetter.js | 6 + .../viewEvent.reducerDescription.js | 71 +++--- src/epics/trackerCapture.epics.js | 2 - 60 files changed, 869 insertions(+), 512 deletions(-) create mode 100644 cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js create mode 100644 src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js delete mode 100644 src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/converter.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js create mode 100644 src/core_modules/capture-core/components/WidgetAssignee/index.js diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js new file mode 100644 index 0000000000..9f45c31e2f --- /dev/null +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js @@ -0,0 +1,52 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; + +When('you assign the user Geetha in the view mode', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-assign"]').click(); + cy.get('[data-test="capture-ui-input"]').type('Geetha'); + cy.contains('Geetha Alwan').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +When('you assign the user Tracker demo User in the edit mode', () => { + cy + .get('[data-test="widget-enrollment-event"]') + .find('[data-test="dhis2-uicore-button"]') + .eq(1) + .click(); + + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-edit"]').click(); + cy.get('[data-test="dhis2-uicore-chip-remove"]').click(); + cy.get('[data-test="capture-ui-input"]').type('Tracker demo'); + cy.contains('Tracker demo User').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +When('you remove the assigned user', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-edit"]').click(); + cy.get('[data-test="dhis2-uicore-chip-remove"]').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +Then('the event has the user Geetha Alwan assigned', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('Geetha Alwan').should('exist'); + }); +}); + +Then('the event has the user Tracker demo User assigned', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('Tracker demo User').should('exist'); + }); +}); + +Then('the event has no assignd user', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('No one is assigned to this event').should('exist'); + }); +}); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature index c3add0d727..27dae1fd6a 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature @@ -102,4 +102,13 @@ Feature: The user interacts with the widgets on the enrollment edit event Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 Then the enrollment widget should be loaded When you click edit mode - Then list should contain the new comment: new test comment \ No newline at end of file + Then list should contain the new comment: new test comment + + Scenario: You can assign a user to a event + Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=veuwiLC2x0e&orgUnitId=g8upMTyEZGZ + When you assign the user Geetha in the view mode + Then the event has the user Geetha Alwan assigned + When you assign the user Tracker demo User in the edit mode + Then the event has the user Tracker demo User assigned + When you remove the assigned user + Then the event has no assignd user diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js index 656f67c564..ff80131398 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js @@ -2,3 +2,5 @@ import '../sharedSteps'; import '../WidgetEnrollment'; import '../WidgetProfile'; import '../WidgetEventComment'; +import '../WidgetAssignee'; + diff --git a/i18n/en.pot b/i18n/en.pot index bca02d95b4..cfe2c464c0 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-01-05T14:09:35.742Z\n" -"PO-Revision-Date: 2024-01-05T14:09:35.742Z\n" +"POT-Creation-Date: 2024-01-22T13:52:51.172Z\n" +"PO-Revision-Date: 2024-01-22T13:52:51.172Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -802,18 +802,18 @@ msgstr "Program Stages could not be loaded" msgid "Stage" msgstr "Stage" -msgid "Enrollment{{escape}} View Event" -msgstr "Enrollment{{escape}} View Event" - -msgid "Enrollment{{escape}} Edit Event" -msgstr "Enrollment{{escape}} Edit Event" - msgid "The enrollment event data could not be found" msgstr "The enrollment event data could not be found" msgid "There are no feedback for this event" msgstr "There are no feedback for this event" +msgid "Enrollment{{escape}} View Event" +msgstr "Enrollment{{escape}} View Event" + +msgid "Enrollment{{escape}} Edit Event" +msgstr "Enrollment{{escape}} Edit Event" + msgid "Registered events" msgstr "Registered events" @@ -926,15 +926,6 @@ msgstr "" "Leaving this page will discard any selections you made for a new " "relationship" -msgid "No one is assigned to this event" -msgstr "No one is assigned to this event" - -msgid "Assign" -msgstr "Assign" - -msgid "Event assigned to {{name}}" -msgstr "Event assigned to {{name}}" - msgid "Feedbacks" msgstr "Feedbacks" @@ -1107,6 +1098,24 @@ msgstr "To work with the selected program," msgid "open the Tracker Capture app" msgstr "open the Tracker Capture app" +msgid "Assigned to" +msgstr "Assigned to" + +msgid "You don't have access to edit this assignee" +msgstr "You don't have access to edit this assignee" + +msgid "Edit" +msgstr "Edit" + +msgid "No one is assigned to this event" +msgstr "No one is assigned to this event" + +msgid "You don't have access to assign an assignee" +msgstr "You don't have access to assign an assignee" + +msgid "Assign" +msgstr "Assign" + msgid "This program is protected" msgstr "This program is protected" @@ -1182,9 +1191,6 @@ msgstr "Latitude" msgid "Longitude" msgstr "Longitude" -msgid "Edit" -msgstr "Edit" - msgid "Set coordinates" msgstr "Set coordinates" @@ -1380,9 +1386,6 @@ msgstr "This stage can only have one event" msgid "Events could not be retrieved. Please try again later." msgstr "Events could not be retrieved. Please try again later." -msgid "Assigned to" -msgstr "Assigned to" - msgid "{{ totalEvents }} events" msgstr "{{ totalEvents }} events" @@ -1578,6 +1581,9 @@ msgstr "Error deleting the enrollment event" msgid "Error editing the event, the changes made were not saved" msgstr "Error editing the event, the changes made were not saved" +msgid "Error updating the Assignee" +msgstr "Error updating the Assignee" + msgid "Set coordinate" msgstr "Set coordinate" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js index d862669ae5..338d00e3ae 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js @@ -72,7 +72,7 @@ export const getAddEventEnrollmentServerData = (state: ReduxState, program: programId, programStage: formFoundation.id, orgUnit: orgUnitId, - trackedEntityInstance: teiId, + trackedEntity: teiId, enrollment: enrollmentId, ...getApiCategoriesArgument(state.currentSelections.categories), dataValues: Object diff --git a/src/core_modules/capture-core/components/FormFields/UserField/index.js b/src/core_modules/capture-core/components/FormFields/UserField/index.js index e534c4387a..e9cb31b656 100644 --- a/src/core_modules/capture-core/components/FormFields/UserField/index.js +++ b/src/core_modules/capture-core/components/FormFields/UserField/index.js @@ -1,3 +1,4 @@ // @flow export { UserField } from './UserField.component'; +export { UserSearch } from './UserSearch.component'; export type { User as UserFormField } from './types'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.js index 4fdb52e652..e9f2dedb08 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.js @@ -105,6 +105,7 @@ const NewEventWorkspacePlain = ({ onSave={onSave} onCancel={onCancel} hideDueDate={stage?.hideDueDate} + enableUserAssignment />} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js new file mode 100644 index 0000000000..ed817f7708 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js @@ -0,0 +1,15 @@ +// @flow + +import { actionCreator } from '../../../actions/actions.utils'; +import type { UserFormField } from '../../FormFields/UserField'; + +export const actionTypes = { + ASSIGNEE_SET: 'EnrollmentDomain.AssigneeSet', + ASSIGNEE_SAVE_FAILED: 'EnrollmentDomain.AssigneeSaveFailed', +}; + +export const setAssignee = (assignedUser?: ApiAssignedUser, assignee: UserFormField | null, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SET)({ assignedUser, assignee, eventId }); + +export const rollbackAssignee = (assignedUser?: ApiAssignedUser, assignee: UserFormField | null, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SAVE_FAILED)({ assignedUser, assignee, eventId }); 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 6137c49b32..16d527e887 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 @@ -14,6 +14,7 @@ import { WidgetFeedback } from '../../WidgetFeedback'; import { WidgetIndicator } from '../../WidgetIndicator'; import { WidgetProfile } from '../../WidgetProfile'; import { WidgetEnrollment } from '../../WidgetEnrollment'; +import { WidgetAssignee } from '../../WidgetAssignee'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; @@ -54,6 +55,121 @@ const styles = ({ typography }) => ({ }, }); +const EnrollmentEditEventPageLeft = ({ + programStage, + teiId, + enrollmentId, + programId, + onGoBack, + orgUnitId, + scheduleDate, + eventStatus, + pageStatus, + onCancelEditEvent, + onHandleScheduleSave, + assignee, +}) => ( + <> + {pageStatus === pageStatuses.DEFAULT && programStage && ( + + )} + {pageStatus === pageStatuses.MISSING_DATA && ( + {i18n.t('The enrollment event data could not be found')} + )} + {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( + + {i18n.t('Choose a registering unit to start reporting')} + + )} + +); + +const EnrollmentEditEventPageRight = ({ + mode, + programStage, + teiId, + enrollmentId, + trackedEntityTypeId, + programId, + widgetEffects, + hideWidgets, + onDelete, + onAddNew, + onLinkedRecordClick, + orgUnitId, + eventAccess, + assignee, + onEnrollmentError, + onEnrollmentSuccess, + getAssignedUserSaveContext, + onSaveAssignee, + onSaveAssigneeError, + addRelationShipContainerElement, + toggleVisibility, +}) => ( + <> + + + + + {!hideWidgets.feedback && ( + + )} + {!hideWidgets.indicator && ( + + )} + {addRelationShipContainerElement && ( + {}} + onLinkedRecordClick={onLinkedRecordClick} + /> + )} + + + +); + const EnrollmentEditEventPagePain = ({ mode, programStage, @@ -75,11 +191,16 @@ const EnrollmentEditEventPagePain = ({ eventDate, scheduleDate, eventStatus, + eventAccess, + assignee, pageStatus, onEnrollmentError, onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, + getAssignedUserSaveContext, + onSaveAssignee, + onSaveAssigneeError, }: PlainProps) => { const [mainContentVisible, setMainContentVisible] = useState(true); const [addRelationShipContainerElement, setAddRelationShipContainerElement] = useState(undefined); @@ -104,10 +225,7 @@ const EnrollmentEditEventPagePain = ({
-
+
{mode === dataEntryKeys.VIEW ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) @@ -115,68 +233,45 @@ const EnrollmentEditEventPagePain = ({
- {pageStatus === pageStatuses.DEFAULT && programStage && ( - - )} - {pageStatus === pageStatuses.MISSING_DATA && ( - {i18n.t('The enrollment event data could not be found')} - )} - {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( - - {i18n.t('Choose a registering unit to start reporting')} - - )} +
- - - - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - {addRelationShipContainerElement && - {}} - onLinkedRecordClick={onLinkedRecordClick} - /> - } - -
diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index 82ee3b9360..7c3de5ae8b 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -17,13 +17,16 @@ import { changeEventFromUrl } from '../ViewEvent/ViewEventComponent/viewEvent.ac import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; import { convertDateWithTimeForView, convertValue } from '../../../converters/clientToView'; import { dataElementTypes } from '../../../metaData/DataElement'; -import { useEvent } from './hooks'; +import { useEvent, useAssignee, useAssignedUserSaveContext } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; import { cleanUpDataEntry } from '../../DataEntry'; import { useLinkedRecordClick } from '../common/TEIRelationshipsWidget'; import { pageKeys } from '../../App/withAppUrlSync'; import { withErrorMessageHandler } from '../../../HOC'; +import { getProgramEventAccess } from '../../../metaData'; +import { setAssignee, rollbackAssignee } from './EnrollmentEditEventPage.actions'; +import { convertClientToServer } from '../../../converters'; const getEventDate = (event) => { const eventDataConvertValue = convertDateWithTimeForView(event?.occurredAt || event?.scheduledAt); @@ -55,6 +58,8 @@ export const EnrollmentEditEventPage = () => { const { loading, event } = useEvent(eventId); const { program: programId, programStage: stageId, trackedEntity: teiId, enrollment: enrollmentId } = event; const { orgUnitId, eventId: urlEventId, initMode } = useLocationQuery(); + const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; + const storedEvent = enrollmentSite?.events?.find(item => item.event === eventId); useEffect(() => { if (!urlEventId) { @@ -65,16 +70,17 @@ export const EnrollmentEditEventPage = () => { } }, [dispatch, history, eventId, urlEventId, orgUnitId]); - return (!loading && eventId === urlEventId) || error ? ( + return ((!loading && eventId === urlEventId) || error) && storedEvent ? ( ) : ; }; @@ -85,11 +91,13 @@ const EnrollmentEditEventPageWithContextPlain = ({ teiId, enrollmentId, orgUnitId, - eventId, initMode, + enrollmentSite, + event, }: Props) => { const history = useHistory(); const dispatch = useDispatch(); + const { event: eventId } = event; const { onLinkedRecordClick } = useLinkedRecordClick(); @@ -119,7 +127,6 @@ const EnrollmentEditEventPageWithContextPlain = ({ } }, [initMode, enrollmentId, eventId, orgUnitId, history]); - const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -131,12 +138,12 @@ const EnrollmentEditEventPageWithContextPlain = ({ // $FlowFixMe const { name: trackedEntityName, id: trackedEntityTypeId } = program?.trackedEntityType; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); - const event = enrollmentSite?.events?.find(item => item.event === eventId); const eventDate = getEventDate(event); const scheduleDate = getEventScheduleDate(event); const { currentPageMode } = useEnrollmentEditEventPageMode(event?.status); const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); + const eventAccess = getProgramEventAccess(programId, programStage?.id); const pageStatus = getPageStatus({ @@ -147,6 +154,20 @@ const EnrollmentEditEventPageWithContextPlain = ({ programStage, event, }); + const assignee = useAssignee(event); + const getAssignedUserSaveContext = useAssignedUserSaveContext(event); + const onSaveAssignee = (newAssignee) => { + // $FlowFixMe dataElementTypes flow error + const assignedUser: ApiAssignedUser = convertClientToServer(newAssignee, dataElementTypes.ASSIGNEE); + dispatch(setAssignee(assignedUser, newAssignee, eventId)); + }; + const onSaveAssigneeError = (prevAssignee) => { + const assignedUser: ApiAssignedUser | typeof undefined = prevAssignee + // $FlowFixMe dataElementTypes flow error + ? convertClientToServer(prevAssignee, dataElementTypes.ASSIGNEE) + : undefined; + dispatch(rollbackAssignee(assignedUser, prevAssignee, eventId)); + }; return ( ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js index 8f269fed00..ed95cad492 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; +import type { UserFormField } from '../../FormFields/UserField'; import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type PlainProps = {| @@ -28,6 +29,14 @@ export type PlainProps = {| onHandleScheduleSave: (eventData: Object) => void, pageStatus: string, eventStatus?: string, + eventAccess: {| + read: boolean, + write: boolean, + |} | null, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + assignee: UserFormField | null, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, ...CssClasses, |}; @@ -37,6 +46,7 @@ export type Props = {| teiId: string, enrollmentId: string, orgUnitId: string, - eventId: string, + event: ApiEnrollmentEvent, + enrollmentSite: ApiEnrollment, initMode?: string, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js index 6ed8386853..c3906733fb 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js @@ -1,2 +1,3 @@ // @flow export { useEvent } from './useEvent'; +export { useAssignee, useAssignedUserSaveContext } from './useAssignedUserSaveContext'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js new file mode 100644 index 0000000000..3819ec28a6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js @@ -0,0 +1,11 @@ +// @flow +import { useMemo, useCallback } from 'react'; +import { dataElementTypes } from '../../../../metaData'; +import { convertServerToClient } from '../../../../converters'; +import type { UserFormField } from '../../../FormFields/UserField'; + +export const useAssignee = (event: ApiEnrollmentEvent): UserFormField | null => + // $FlowFixMe dataElementTypes flow error + useMemo(() => convertServerToClient(event?.assignedUser, dataElementTypes.ASSIGNEE), [event?.assignedUser]); + +export const useAssignedUserSaveContext = (event: ApiEnrollmentEvent) => useCallback(() => ({ event }), [event]); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js index 3ce5e6a217..5a8c768ab0 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js @@ -1,3 +1,6 @@ // @flow export { EnrollmentEditEventPage } from './EnrollmentEditEventPage.container'; export { updateEventSucceededEpic, updateEventFailedEpic } from './EnrollmentEditEventPage.epics'; +export { + actionTypes as enrollmentEditEventActionTypes, +} from './EnrollmentEditEventPage.actions'; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js deleted file mode 100644 index dde335035b..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { IconUser24 } from '@dhis2/ui'; -import { ViewEventSection } from '../../Section/ViewEventSection.component'; -import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.component'; -import { Contents } from './Contents.component'; -import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; -import { type ProgramStage } from '../../../../../metaData'; - -const LoadingContents = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Contents); - -type Props = { - programStage: ProgramStage, - classes: Object, -} - -const loadingIndicatorStyle = { - height: 36, - width: 36, -}; - -export class AssigneeSectionComponent extends React.Component { - renderHeader = () => ( - - ) - - render() { - const { programStage, ...passOnProps } = this.props; - - if (!programStage.enableUserAssignment) { - return null; - } - - return ( - - {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - - - ); - } -} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js index b22b90aa22..4698089e83 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js @@ -1,25 +1,35 @@ // @flow -import { connect } from 'react-redux'; -import { AssigneeSectionComponent } from './AssigneeSection.component'; -import { setAssignee } from './assigneeSection.actions'; +import React from 'react'; +import { WidgetAssignee } from '../../../../WidgetAssignee'; +import type { ProgramStage } from '../../../../../metaData'; +import type { UserFormField } from '../../../../FormFields/UserField'; -const mapStateToProps = (state: ReduxState) => { - const assigneeSection = state.viewEventPage.assigneeSection || {}; +type Props = {| + assignee: UserFormField | null, + programStage: ?ProgramStage, + eventAccess: {| + read: boolean, + write: boolean, + |} | null, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, +|}; - return { - assignee: (!assigneeSection.isLoading) ? - state.viewEventPage.loadedValues.eventContainer.event.assignee : - undefined, - ready: !assigneeSection.isLoading, - }; -}; - -const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ - onSet: (user: Object) => { - dispatch(setAssignee(user)); - }, -}); - -// $FlowSuppress -// $FlowFixMe[missing-annot] automated comment -export const AssigneeSection = connect(mapStateToProps, mapDispatchToProps)(AssigneeSectionComponent); +export const AssigneeSection = ({ + assignee, + programStage, + getAssignedUserSaveContext, + eventAccess, + onSaveAssignee, + onSaveAssigneeError, +}: Props) => ( + +); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js deleted file mode 100644 index 5d5daf12aa..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import * as React from 'react'; -import { DisplayMode } from './DisplayMode.component'; -import { EditMode } from './EditMode.component'; - -type Props = { - onSet: (user: Object) => void, -}; - -export const Contents = (props: Props) => { - const { onSet, ...passOnProps } = props; - const [editMode, setEditMode] = React.useState(false); - - const handleSet = React.useCallback((user) => { - setEditMode(false); - onSet(user); - }, [onSet]); - - const handleCancelSearch = React.useCallback(() => { - setEditMode(false); - }, []); - - if (editMode) { - return ( - - ); - } - - return ( - // $FlowFixMe[cannot-spread-inexact] automated comment - { setEditMode(true); }} - {...passOnProps} - /> - ); -}; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js deleted file mode 100644 index 394275ea37..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js +++ /dev/null @@ -1,90 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { withStyles, IconButton } from '@material-ui/core'; -import { IconEdit24, Button } from '@dhis2/ui'; - -const getStyles = () => ({ - container: { - display: 'flex', - alignItems: 'center', - }, - nameContainer: { - paddingRight: 5, - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - iconContainer: { - width: 24, - }, - editButton: { - color: 'inherit', - }, - addIcon: { - paddingRight: 5, - }, -}); - -type User = { - id: string, - username: string, - name: string, -}; - -type Props = { - assignee: ?User, - onEdit: () => void, - classes: Object, - eventAccess: { read: boolean, write: boolean }, -}; - -const DisplayModePlain = (props: Props) => { - const { eventAccess, assignee, onEdit, classes } = props; - - if (!assignee) { - if (!eventAccess.write) { - return ( -
- {i18n.t('No one is assigned to this event')} -
- ); - } - return ( -
- -
- ); - } - - return ( -
-
- {i18n.t('Event assigned to {{name}}', { name: assignee.name })} -
-
- {eventAccess.write ? - ( - - - - ) : null} -
-
- ); -}; - -export const DisplayMode = withStyles(getStyles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js deleted file mode 100644 index 41de1ddd9a..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { Button } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; -import { UserSearch } from '../../../../FormFields/UserField/UserSearch.component'; - -const getStyles = () => ({ - container: { - display: 'flex', - alignItems: 'center', - }, - searchContainer: { - flexGrow: 1, - flexShrink: 1, - paddingRight: 5, - }, - buttonContainer: { - flexGrow: 0, - flexShrink: 0, - }, -}); - -type Props = { - onCancel: Function, - classes: Object, -}; - -const EditModePlain = (props: Props) => { - const { onCancel, classes, ...passOnProps } = props; - return ( -
-
- {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - -
-
- -
-
- ); -}; - -export const EditMode = withStyles(getStyles)(EditModePlain); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js deleted file mode 100644 index 0bfd4a1db2..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -import { actionCreator } from '../../../../../actions/actions.utils'; -import { effectMethods } from '../../../../../trackerOffline'; - -export const actionTypes = { - VIEW_EVENT_ASSIGNEE_SET: 'ViewEventAssigneeSet', - VIEW_EVENT_ASSIGNEE_SAVE: 'ViewEventAssigneeSave', - VIEW_EVENT_ASSIGNEE_SAVE_COMPLETED: 'ViewEventAssigneeSaveCompleted', - VIEW_EVENT_ASSIGNEE_SAVE_FAILED: 'ViewEventAssigneeSaveFailed', -}; - -export const setAssignee = (assignee: Object) => - actionCreator(actionTypes.VIEW_EVENT_ASSIGNEE_SET)({ assignee }); - -export const saveAssignee = (eventId: string, serverData: Object, selections: Object) => - actionCreator(actionTypes.VIEW_EVENT_ASSIGNEE_SAVE)({}, { - offline: { - effect: { - url: 'tracker?async=false&importStrategy=UPDATE', - method: effectMethods.POST, - data: serverData, - }, - commit: { type: actionTypes.VIEW_EVENT_ASSIGNEE_SAVE_COMPLETED, meta: { eventId, selections } }, - rollback: { type: actionTypes.VIEW_EVENT_ASSIGNEE_SAVE_FAILED, meta: { eventId, selections } }, - }, - }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js index 5abb01a6c0..9646d16bb6 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js @@ -1,4 +1,2 @@ // @flow -export { actionTypes as assigneeSectionActionTypes } from './assigneeSection.actions'; export { AssigneeSection } from './AssigneeSection.container'; -export { saveAssigneeEpic } from './saveAssignee.epic'; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js deleted file mode 100644 index 99f4a3348b..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js +++ /dev/null @@ -1,37 +0,0 @@ -// @flow -import { ofType } from 'redux-observable'; -import { map } from 'rxjs/operators'; -import { actionTypes, saveAssignee } from './assigneeSection.actions'; -import { getEventProgramThrowIfNotFound } from '../../../../../metaData'; -import { convertValue as convertToServerValue } from '../../../../../converters/clientToServer'; -import { convertMainEventClientToServer } from '../../../../../events/mainConverters'; - -export const saveAssigneeEpic = (action$: InputObservable, store: ReduxStore) => - action$.pipe( - ofType(actionTypes.VIEW_EVENT_ASSIGNEE_SET), - map(() => { - const state = store.value; - const eventId = state.viewEventPage.eventId; - const eventContainer = state.viewEventPage.loadedValues.eventContainer; - const { event: clientMainValues, values: clientValues } = eventContainer; - const program = getEventProgramThrowIfNotFound(clientMainValues.programId); - const formFoundation = program.stage.stageForm; - const formServerValues = formFoundation.convertValues(clientValues, convertToServerValue); - const mainDataServerValues: Object = convertMainEventClientToServer(clientMainValues); - - const serverData = { - events: [{ - ...mainDataServerValues, - dataValues: Object - .keys(formServerValues) - .map(key => ({ - dataElement: key, - value: formServerValues[key], - })), - }], - }; - - const currentSelectionSet = state.currentSelections; - - return saveAssignee(eventId, serverData, currentSelectionSet); - })); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js index 307d8ccd30..d04213a437 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js @@ -9,7 +9,7 @@ import { FeedbacksSection } from './FeedbacksSection/FeedbacksSection.container' import { IndicatorsSection } from './IndicatorsSection/IndicatorsSection.container'; import { RelationshipsSection } from './RelationshipsSection/RelationshipsSection.container'; import { NotesSection } from './NotesSection/NotesSection.container'; -import { AssigneeSection } from './AssigneeSection/AssigneeSection.container'; +import { AssigneeSection } from './AssigneeSection'; type Props = { classes: { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js index dbb2406dff..de6353d5ea 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js @@ -8,7 +8,7 @@ import { RightColumnWrapper } from '../RightColumn/RightColumnWrapper.component' import type { ProgramStage } from '../../../../metaData'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { defaultDialogProps } from '../../../Dialogs/DiscardDialog.constants'; - +import type { UserFormField } from '../../../FormFields/UserField'; const getStyles = (theme: Theme) => ({ container: { @@ -48,6 +48,10 @@ type Props = { header: string, showAllEvents: string, }, + assignee: UserFormField, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, }; type State = { @@ -70,7 +74,17 @@ class ViewEventPlain extends Component { } render() { - const { classes, programStage, currentDataEntryKey, eventAccess } = this.props; + const { + classes, + programStage, + currentDataEntryKey, + eventAccess, + assignee, + getAssignedUserSaveContext, + onSaveAssignee, + onSaveAssigneeError, + } = this.props; + return (
{ const programStageSelector = makeProgramStageSelector(); const eventAccessSelector = makeEventAccessSelector(); + const assignedUserContextSelector = makeAssignedUserContextSelector(); // $FlowFixMe[not-an-object] automated comment return (state: ReduxState) => { @@ -29,6 +31,9 @@ const makeMapStateToProps = () => { error: state.viewEventPage.loadError, currentDataEntryKey, isUserInteractionInProgress, + assignee: state.viewEventPage.loadedValues?.eventContainer.event.assignee, + getAssignedUserSaveContext: () => assignedUserContextSelector(state), + eventId: state.viewEventPage.eventId, }; }; }; @@ -37,10 +42,26 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ onBackToAllEvents: () => { dispatch(startGoBackToMainPage()); }, + dispatch, }); +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const mergedProps = { + onSaveAssignee: (newAssignee) => { + dispatchProps.dispatch(setAssignee(newAssignee, stateProps.eventId)); + }, + onSaveAssigneeError: (prevAssignee) => { + dispatchProps.dispatch(rollbackAssignee(prevAssignee, stateProps.eventId)); + }, + }; + + return Object.assign({}, ownProps, stateProps, dispatchProps, mergedProps); +}; + // $FlowSuppress // $FlowFixMe[missing-annot] automated comment -export const ViewEvent = connect(makeMapStateToProps, mapDispatchToProps)( - withErrorMessageHandler()(ViewEventComponent), -); +export const ViewEvent = connect( + makeMapStateToProps, + mapDispatchToProps, + mergeProps, +)(withErrorMessageHandler()(ViewEventComponent)); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js index 2c4c627067..14413d97bc 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js @@ -1,6 +1,7 @@ // @flow import { actionCreator } from 'capture-core/actions/actions.utils'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; +import type { UserFormField } from '../../../FormFields/UserField'; export const actionTypes = { VIEW_EVENT_FROM_URL: 'ViewEventFromUrl', @@ -19,6 +20,8 @@ export const actionTypes = { UPDATE_WORKING_LIST_PENDING_ON_BACK_TO_MAIN_PAGE: 'UpdateWorkingListPendingOnBackToMainPageForViewEvent', OPEN_VIEW_EVENT_PAGE_FAILED: 'OpenViewEventPageFailed', INITIALIZE_WORKING_LISTS_ON_BACK_TO_MAIN_PAGE: 'InitializeWorkingListsOnBackToMainPage', + ASSIGNEE_SET: 'SingleEvent.AssigneeSet', + ASSIGNEE_SAVE_FAILED: 'SingleEvent.AssigneeSaveFailed', }; export const viewEventFromUrl = (data: Object) => @@ -74,3 +77,10 @@ export const updateEventContainer = (eventContainer: Object, orgUnit: OrgUnit) = export const openViewEventPageFailed = (error: string) => actionCreator(actionTypes.OPEN_VIEW_EVENT_PAGE_FAILED)({ error }); + +export const setAssignee = (assignee: UserFormField, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SET)({ assignee, eventId }); + +export const rollbackAssignee = (assignee: UserFormField, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SAVE_FAILED)({ assignee, eventId }); + diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js index 69cd29f1f6..afb7761610 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js @@ -2,10 +2,12 @@ import { createSelector } from 'reselect'; import { getEventProgramEventAccess, getEventProgramThrowIfNotFound } from '../../../../metaData'; - +import { convertValue as convertToServerValue } from '../../../../converters/clientToServer'; +import { convertMainEventClientToServer } from '../../../../events/mainConverters'; const programIdSelector = state => state.currentSelections.programId; const categoriesMetaSelector = state => state.currentSelections.categoriesMeta; +const eventContainerSelector = state => state.viewEventPage.loadedValues?.eventContainer; // $FlowFixMe[missing-annot] automated comment export const makeProgramStageSelector = () => createSelector( @@ -18,3 +20,23 @@ export const makeEventAccessSelector = () => createSelector( categoriesMetaSelector, (programId: string, categoriesMeta: ?Object) => getEventProgramEventAccess(programId, categoriesMeta)); +export const makeAssignedUserContextSelector = () => + // $FlowFixMe[missing-annot] + createSelector(eventContainerSelector, (eventContainer) => { + const { event: clientMainValues, values: clientValues } = eventContainer; + const program = getEventProgramThrowIfNotFound(clientMainValues.programId); + const formFoundation = program.stage.stageForm; + const formServerValues = formFoundation.convertValues(clientValues, convertToServerValue); + const mainDataServerValues: Object = convertMainEventClientToServer(clientMainValues); + + const event = + { + ...mainDataServerValues, + dataValues: Object.keys(formServerValues).map(key => ({ + dataElement: key, + value: formServerValues[key], + })), + }; + + return { event }; + }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/index.js b/src/core_modules/capture-core/components/Pages/ViewEvent/index.js index 681239203a..082ff0bd70 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/index.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/index.js @@ -1,5 +1,3 @@ // @flow export { actionTypes as editEventDataEntryActionTypes } from '../../WidgetEventEdit/EditEventDataEntry'; -export { assigneeSectionActionTypes } from '../ViewEvent/RightColumn/AssigneeSection'; - export { ViewEventPage } from './ViewEventPage.container'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index 688e293955..6a4a067870 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -17,9 +17,11 @@ export type Event = {| program: string, programStage: string, status: 'ACTIVE' | 'VISITED' | 'COMPLETED' | 'SCHEDULE' | 'OVERDUE' | 'SKIPPED', - trackedEntityInstance: string, + trackedEntity: string, notes?: Array, pendingApiResponse?: ?boolean, + assignedUser?: ApiAssignedUser, + followUp?: boolean, |}; export type EnrollmentData = {| diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js index d8ff56e1b7..f1d24d1b6f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js @@ -33,7 +33,7 @@ const useEventsData = (enrollment, program) => { programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - trackedEntityInstanceId: event.trackedEntityInstance, + trackedEntityInstanceId: event.trackedEntity, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, status: event.status, diff --git a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js new file mode 100644 index 0000000000..e0b2ebb2dd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js @@ -0,0 +1,84 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button, spacers, UserAvatar } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; +import type { Assignee } from './WidgetAssignee.types'; + +const styles = () => ({ + wrapper: { + display: 'flex', + alignItems: 'center', + fontSize: 14, + }, + editButton: { + marginLeft: spacers.dp12, + }, + assignButton: { + marginLeft: spacers.dp12, + }, + avatarWrapper: { + display: 'flex', + alignItems: 'center', + }, + avatar: { + margin: spacers.dp4, + }, +}); + +type Props = { + assignee: Assignee | null, + onEdit: () => {}, + writeAccess: boolean, + avatarId?: string, + ...CssClasses, +}; + +const DisplayModePlain = ({ assignee, onEdit, writeAccess, avatarId, classes }: Props) => ( + assignee ? ( +
+
+ {i18n.t('Assigned to')} + + {assignee.name} +
+ + + +
+ ) : ( +
+ {i18n.t('No one is assigned to this event')} + + + +
+ ) +); + +export const DisplayMode = withStyles(styles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js b/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js new file mode 100644 index 0000000000..dd7202f18c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js @@ -0,0 +1,63 @@ +// @flow +import React, { useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button, ButtonStrip, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import type { Assignee } from './WidgetAssignee.types'; +import { UserField } from '../FormFields/UserField'; + +const styles = () => ({ + container: { + display: 'flex', + alignItems: 'center', + }, + searchContainer: { + flexGrow: 1, + flexShrink: 1, + paddingRight: 5, + }, + buttonContainer: { + marginTop: spacers.dp8, + }, +}); + +type Props = { + assignee: Assignee | null, + onCancel: () => {}, + onSet: (user: Assignee | null) => void, + ...CssClasses, +}; + +const EditModePlain = (props: Props) => { + const { onCancel, onSet, assignee, classes } = props; + const [tempUser, setTempUser] = useState(assignee); + + const onHandleSet = (user) => { + setTempUser(user); + }; + + return ( +
+
+ + + + + +
+
+ ); +}; + +export const EditMode = withStyles(styles)(EditModePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js new file mode 100644 index 0000000000..2b54a45fcd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js @@ -0,0 +1,62 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { IconUser24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import type { PlainProps } from './WidgetAssignee.types'; +import { Widget } from '../Widget'; +import { DisplayMode } from './DisplayMode.component'; +import { EditMode } from './EditMode.component'; + +const styles = () => ({ + header: { + display: 'flex', + alignItems: 'center', + }, + wrapper: { + padding: `0 ${spacers.dp16} ${spacers.dp16} ${spacers.dp16}`, + }, +}); + +const WidgetAssigneePlain = ({ assignee, writeAccess, onSet, avatarId, classes }: PlainProps) => { + const [open, setOpenStatus] = useState(true); + const [editMode, setEditMode] = useState(false); + + const handleSet = useCallback( + (user) => { + setEditMode(false); + onSet(user); + }, + [onSet], + ); + + return ( +
+ + {i18n.t('Assignee')} + + } + onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} + onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} + open={open} + > +
+ {editMode ? ( + setEditMode(false)} onSet={handleSet} assignee={assignee} /> + ) : ( + setEditMode(true)} + writeAccess={writeAccess} + avatarId={avatarId} + /> + )} +
+
+
+ ); +}; + +export const WidgetAssigneeComponent = withStyles(styles)(WidgetAssigneePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js new file mode 100644 index 0000000000..53910eac42 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js @@ -0,0 +1,50 @@ +// @flow +import React, { useCallback, useRef } from 'react'; +import { useDataMutation } from '@dhis2/app-runtime'; +import type { Props, Assignee } from './WidgetAssignee.types'; +import { WidgetAssigneeComponent } from './WidgetAssignee.component'; +import { convertClientToServer } from './converter'; +import { useUserAvatar } from './hooks'; + +const WidgetAssigneeWithHooks = (props: Props) => { + const { assignee, writeAccess, getSaveContext, onSave, onSaveError } = props; + const prevAssignee = useRef(assignee); + const { avatarId, isLoading } = useUserAvatar(assignee?.id); + + const [updateMutation] = useDataMutation( + { + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: event => ({ events: [event] }), + }, + { + onError: () => { + onSaveError(prevAssignee.current); + }, + }, + ); + + const onSet = useCallback( + async (newAssignee: Assignee) => { + const { event } = getSaveContext(); + prevAssignee.current = assignee; + onSave(newAssignee); + await updateMutation({ ...event, assignedUser: convertClientToServer(newAssignee) }); + }, + [updateMutation, getSaveContext, onSave, assignee], + ); + + if (isLoading) { + return null; + } + + return ; +}; + +export const WidgetAssignee = (props: Props) => { + if (!props.enabled) { + return null; + } + + return ; +}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js new file mode 100644 index 0000000000..33eb25a570 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js @@ -0,0 +1,26 @@ +// @flow + +export type Assignee = { + id: string, + username: string, + name: string, + firstName: string, + surname: string, +} + +export type Props = {| + assignee: Assignee | null, + enabled: boolean, + writeAccess: boolean, + getSaveContext: () => { event: ApiEnrollmentEvent }, + onSave: (newAssignee: Assignee) => void, + onSaveError: (prevAssignee: Assignee | null) => void, +|}; + +export type PlainProps = {| + assignee: Assignee | null, + writeAccess: boolean, + onSet: (user: Assignee | null) => void, + avatarId?: string, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/converter.js b/src/core_modules/capture-core/components/WidgetAssignee/converter.js new file mode 100644 index 0000000000..735b8ddfb2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/converter.js @@ -0,0 +1,14 @@ +// @flow +import type { Assignee } from './WidgetAssignee.types'; + +export const convertClientToServer = (assignee?: Assignee): ApiAssignedUser | null => ( + assignee + ? { + uid: assignee.id, + displayName: assignee.name, + username: assignee.username, + firstName: assignee.firstName, + surname: assignee.surname, + } + : null +); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js b/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js new file mode 100644 index 0000000000..d6927c9a30 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js @@ -0,0 +1,2 @@ +// @flow +export { useUserAvatar } from './useUserAvatar'; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js b/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js new file mode 100644 index 0000000000..2e66e93561 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js @@ -0,0 +1,20 @@ +// @flow +import { useApiMetadataQuery } from 'capture-core/utils/reactQueryHelpers'; + +export const useUserAvatar = (userId?: string) => { + const queryKey = ['users', ...(userId ? [userId] : [])]; + const queryFn = { + resource: 'users', + id: userId, + params: { + fields: 'avatar', + }, + }; + const queryOptions = { enabled: Boolean(userId) }; + const { data, isLoading } = useApiMetadataQuery(queryKey, queryFn, queryOptions); + + return { + avatarId: data?.avatar?.id, + isLoading, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/index.js b/src/core_modules/capture-core/components/WidgetAssignee/index.js new file mode 100644 index 0000000000..3118669d83 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/index.js @@ -0,0 +1,3 @@ +// @flow +export { WidgetAssignee } from './WidgetAssignee.container'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js index f1eeab8b32..db0b0e9af7 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js @@ -44,7 +44,7 @@ export const getAddEventEnrollmentServerData = ({ program: programId, programStage: formFoundation.id, orgUnit: orgUnitId, - trackedEntityInstance: teiId, + trackedEntity: teiId, enrollment: enrollmentId, scheduledAt: mainDataServerValues.occurredAt, orgUnitName, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index 5542c3e8d4..3f39dd6b3f 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -48,6 +48,7 @@ import { withDataEntryFields, } from '../../DataEntryDhis2Helpers/'; import { getProgramThrowIfNotFound, EventProgram } from '../../../metaData'; +import type { UserFormField } from '../../FormFields/UserField'; const tabMode = Object.freeze({ REPORT: 'REPORT', @@ -398,6 +399,7 @@ type Props = { eventStatus?: string, enrollmentId?: string, isCompleted?: boolean, + assignee?: UserFormField | null, }; @@ -458,6 +460,7 @@ class EditEventDataEntryPlain extends Component { classes, dataEntryId, onCancelEditEvent, + assignee, ...passOnProps } = this.props; @@ -485,6 +488,7 @@ class EditEventDataEntryPlain extends Component { orgUnitId={orgUnit.id} onSaveSuccessActionType={actionTypes.EVENT_SCHEDULE_SUCCESS} onSaveErrorActionType={actionTypes.EVENT_SCHEDULE_ERROR} + assignee={assignee} {...passOnProps} />} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js index 58c4645463..4075f5d0da 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js @@ -58,6 +58,7 @@ export const WidgetEventEditPlain = ({ orgUnitId, enrollmentId, teiId, + assignee, }: Props) => { const dispatch = useDispatch(); const { currentPageMode } = useEnrollmentEditEventPageMode(eventStatus); @@ -144,6 +145,7 @@ export const WidgetEventEditPlain = ({ allowGenerateNextVisit={programStage.allowGenerateNextVisit} availableProgramStages={availableProgramStages} hideDueDate={programStage.hideDueDate} + assignee={assignee} /> )} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js index 8dbc06e0c6..01e4caade5 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../metaData'; +import type { UserFormField } from '../FormFields/UserField'; export type Props = {| programStage: ProgramStage, @@ -13,5 +14,6 @@ export type Props = {| enrollmentId: string, teiId: string, initialScheduleDate?: string, + assignee?: UserFormField | null, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js index fdbbccd9f2..e9a1111920 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js @@ -4,4 +4,5 @@ import type { UserFormField } from '../../FormFields/UserField'; export type Props = { ...CssClasses, assignee?: UserFormField, + onSetAssignee: (user: UserFormField) => void, }; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js index 6dc9c0a5bc..cf7b02e10d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js @@ -35,7 +35,7 @@ export const requestScheduleEvent = ({ onSaveExternal: (eventServerValues: Object, uid: string) => void, onSaveSuccessActionType?: string, onSaveErrorActionType?: string, - assignedUser?: {uid: string}, + assignedUser?: ApiAssignedUser | null, }) => actionCreator(scheduleEventWidgetActionTypes.EVENT_SCHEDULE_REQUEST)({ scheduleDate, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 489ce42dbc..27cf5bc8b2 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import moment from 'moment'; -import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; @@ -18,7 +18,7 @@ import { import { requestScheduleEvent } from './WidgetEventSchedule.actions'; import { NoAccess } from './AccessVerification'; import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; -import { convertAssigneeToServer } from '../../converters'; +import { convertClientToServer } from '../../converters'; export const WidgetEventSchedule = ({ enrollmentId, @@ -31,6 +31,8 @@ export const WidgetEventSchedule = ({ onSaveErrorActionType, onCancel, initialScheduleDate, + enableUserAssignment, + assignee: storedAssignee, ...passOnProps }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); @@ -44,7 +46,7 @@ export const WidgetEventSchedule = ({ const { currentUser, noteId } = useCommentDetails(); const [scheduleDate, setScheduleDate] = useState(''); const [comments, setComments] = useState([]); - const [assignee, setAssignee] = useState(); + const [assignee, setAssignee] = useState(storedAssignee); const { events } = useEventsInOrgUnit(orgUnitId, scheduleDate); const { eventId } = useLocationQuery(); const eventCountInOrgUnit = events @@ -57,6 +59,10 @@ export const WidgetEventSchedule = ({ if (!scheduleDate && suggestedScheduleDate) { setScheduleDate(suggestedScheduleDate); } }, [suggestedScheduleDate, scheduleDate]); + useEffect(() => { + setAssignee(storedAssignee); + }, [storedAssignee]); + const onHandleSchedule = useCallback(() => { if (programCategory?.categories && Object.keys(selectedCategories).length !== programCategory?.categories?.length) { @@ -82,7 +88,8 @@ export const WidgetEventSchedule = ({ onSaveExternal: onSave, onSaveSuccessActionType, onSaveErrorActionType, - ...(assignee && { assignedUser: convertAssigneeToServer(assignee) }), + // $FlowFixMe[incompatible-call] + ...(assignee && { assignedUser: convertClientToServer(assignee, dataElementTypes.ASSIGNEE) }), })); }, [ dispatch, @@ -170,7 +177,7 @@ export const WidgetEventSchedule = ({ programId={programId} programCategory={programCategory} programName={program.name} - enableUserAssignment={stage?.enableUserAssignment} + enableUserAssignment={enableUserAssignment && stage?.enableUserAssignment} scheduleDate={scheduleDate} displayDueDateLabel={programStageScheduleConfig.displayDueDateLabel} suggestedScheduleDate={suggestedScheduleDate} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js index f42a7e8f87..39b5789226 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js @@ -19,6 +19,8 @@ export type ContainerProps = {| onSaveSuccessActionType: string, onSaveErrorActionType: string, onCancel: () => void, + assignee?: UserFormField | null, + enableUserAssignment?: boolean, |}; export type Props = {| @@ -41,8 +43,8 @@ export type Props = {| categoryOptionsError?: ?{[categoryId: string]: { touched: boolean, valid: boolean} }, enableUserAssignment?: boolean, onSchedule: () => void, - onSetAssignee: (user: UserFormField) => void, - assignee?: UserFormField, + onSetAssignee: () => void, + assignee?: UserFormField | null, onCancel: () => void, setScheduleDate: (date: string) => void, onAddComment: (comment: string) => void, diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js index 6e606a08d4..0f657aac4f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/hooks/useEvents.js @@ -35,7 +35,7 @@ export const useEvents = (enrollment: any, elementsById: Array) => { programStageId: event.programStage, orgUnitId: event.orgUnit, orgUnitName: orgUnitNames[event.orgUnit], - trackedEntityInstanceId: event.trackedEntityInstance, + trackedEntityInstanceId: event.trackedEntity, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, status: event.status, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js index ef1a90c811..b1016ab49f 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/types/common.types.js @@ -53,7 +53,9 @@ export type Event = {| program: string, programStage: string, status: 'ACTIVE' | 'VISITED' | 'COMPLETED' | 'SCHEDULE' | 'OVERDUE' | 'SKIPPED', - trackedEntityInstance: string, + trackedEntity: string, notes?: Array, pendingApiResponse?: ?boolean, + assignedUser?: ApiAssignedUser, + followUp?: boolean, |}; diff --git a/src/core_modules/capture-core/converters/clientToServer.js b/src/core_modules/capture-core/converters/clientToServer.js index 49b4af1021..c59c2580ec 100644 --- a/src/core_modules/capture-core/converters/clientToServer.js +++ b/src/core_modules/capture-core/converters/clientToServer.js @@ -30,6 +30,17 @@ function convertRange(parser: (value: any) => any, rangeValue: RangeValue) { }; } +const convertAssigneeToServer = (assignee?: Assignee | null): ApiAssignedUser | null => + (assignee + ? { + uid: assignee.id, + displayName: assignee.name, + username: assignee.username, + firstName: assignee.firstName, + surname: assignee.surname, + } + : null); + const valueConvertersForType = { [dataElementTypes.NUMBER]: stringifyNumber, [dataElementTypes.NUMBER_RANGE]: (value: RangeValue) => convertRange(stringifyNumber, value), @@ -51,6 +62,7 @@ const valueConvertersForType = { [dataElementTypes.COORDINATE]: (rawValue: Object) => `[${rawValue.longitude},${rawValue.latitude}]`, [dataElementTypes.ORGANISATION_UNIT]: (rawValue: Object) => rawValue.id, [dataElementTypes.AGE]: (rawValue: Object) => convertDate(rawValue), + [dataElementTypes.ASSIGNEE]: convertAssigneeToServer, }; export function convertValue(value: any, type: $Keys) { @@ -73,13 +85,3 @@ export function convertCategoryOptionsToServer(value: {[categoryId: string]: str } return value; } - -export function convertAssigneeToServer(assignee: Assignee): ApiAssignedUser { - return { - uid: assignee.id, - displayName: assignee.name, - username: assignee.username, - firstName: assignee.firstName, - surname: assignee.surname, - }; -} diff --git a/src/core_modules/capture-core/converters/index.js b/src/core_modules/capture-core/converters/index.js index 82eb3936a6..74b4d0e019 100644 --- a/src/core_modules/capture-core/converters/index.js +++ b/src/core_modules/capture-core/converters/index.js @@ -2,7 +2,7 @@ export { convertValue as convertClientToForm } from './clientToForm'; export { convertValue as convertClientToList } from './clientToList'; export { convertValue as convertClientToView, convertDateWithTimeForView } from './clientToView'; -export { convertValue as convertClientToServer, convertAssigneeToServer } from './clientToServer'; +export { convertValue as convertClientToServer } from './clientToServer'; export { convertValue as convertFormToClient } from './formToClient'; export { convertValue as convertServerToClient, diff --git a/src/core_modules/capture-core/converters/serverToClient.js b/src/core_modules/capture-core/converters/serverToClient.js index 288948461e..696feca4b7 100644 --- a/src/core_modules/capture-core/converters/serverToClient.js +++ b/src/core_modules/capture-core/converters/serverToClient.js @@ -14,7 +14,7 @@ function convertTime(d2Value: string) { const convertAssignedUserToClient = (assignedUser?: ApiAssignedUser) => ((assignedUser && assignedUser.uid) ? { id: assignedUser.uid, - name: assignedUser.displayName, + name: assignedUser.displayName || `${assignedUser.firstName} ${assignedUser.surname}`, username: assignedUser.username, firstName: assignedUser.firstName, surname: assignedUser.surname, diff --git a/src/core_modules/capture-core/events/eventRequests.js b/src/core_modules/capture-core/events/eventRequests.js index e598b93a16..4039fc3895 100644 --- a/src/core_modules/capture-core/events/eventRequests.js +++ b/src/core_modules/capture-core/events/eventRequests.js @@ -60,7 +60,7 @@ const mapEventInputKeyToOutputKey = { program: 'programId', programStage: 'programStageId', orgUnit: 'orgUnitId', - trackedEntityInstance: 'trackedEntityInstanceId', + trackedEntity: 'trackedEntityId', enrollment: 'enrollmentId', }; diff --git a/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js b/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js index 207918b8cb..4b7344eac9 100644 --- a/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js +++ b/src/core_modules/capture-core/events/mainConverters/mainEventClientToServerConverter.js @@ -1,5 +1,5 @@ // @flow -import { convertClientToServer, convertAssigneeToServer } from '../../converters'; +import { convertClientToServer } from '../../converters'; import { convertMainEvent } from './mainEventConverter'; import { dataElementTypes } from '../../metaData'; import { convertEventAttributeOptions } from '../convertEventAttributeOptions'; @@ -10,7 +10,7 @@ export function convertMainEventClientToServer(event: Object) { programId: 'program', programStageId: 'programStage', orgUnitId: 'orgUnit', - trackedEntityInstanceId: 'trackedEntityInstance', + trackedEntityId: 'trackedEntity', enrollmentId: 'enrollment', assignee: 'assignedUser', }; @@ -26,7 +26,7 @@ export function convertMainEventClientToServer(event: Object) { convertedValue = convertClientToServer(value, dataElementTypes.DATE); break; case 'assignee': - convertedValue = value && convertAssigneeToServer(value); + convertedValue = value && convertClientToServer(value, dataElementTypes.ASSIGNEE); break; default: convertedValue = value; diff --git a/src/core_modules/capture-core/events/prepareEnrollmentEvents.js b/src/core_modules/capture-core/events/prepareEnrollmentEvents.js index c059182f99..31e19524e1 100644 --- a/src/core_modules/capture-core/events/prepareEnrollmentEvents.js +++ b/src/core_modules/capture-core/events/prepareEnrollmentEvents.js @@ -24,7 +24,7 @@ const mapEventInputKeyToOutputKey = { program: 'programId', programStage: 'programStageId', orgUnit: 'orgUnitId', - trackedEntityInstance: 'trackedEntityInstanceId', + trackedEntity: 'trackedEntityId', enrollment: 'enrollmentId', }; diff --git a/src/core_modules/capture-core/flow/apiTypes.js b/src/core_modules/capture-core/flow/apiTypes.js index 23766ea4a4..491870d8c2 100644 --- a/src/core_modules/capture-core/flow/apiTypes.js +++ b/src/core_modules/capture-core/flow/apiTypes.js @@ -3,7 +3,7 @@ declare type ApiAssignedUser = {| uid: string, username: string, - displayName: string, + displayName?: string, firstName: string, surname: string, |}; @@ -18,7 +18,7 @@ declare type ApiEnrollmentEvent = {| program: string, programStage: string, orgUnit: string, - trackedEntityInstance: string, + trackedEntity: string, enrollment: string, enrollmentStatus: string, status: 'ACTIVE' | 'VISITED' | 'COMPLETED' | 'SCHEDULE' | 'OVERDUE' | 'SKIPPED', @@ -29,6 +29,8 @@ declare type ApiEnrollmentEvent = {| notes?: Array, deleted?: boolean, pendingApiResponse?: ?boolean, + assignedUser?: ApiAssignedUser, + followUp?: boolean, |}; type ApiAttributeValues = { 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 df930fe486..71761ba0f1 100644 --- a/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/enrollmentDomain.reducerDescription.js @@ -4,6 +4,7 @@ import { enrollmentSiteActionTypes } from '../../components/Pages/common/Enrollm import { actionTypes as enrollmentNoteActionTypes } from '../../components/WidgetEnrollmentComment/WidgetEnrollmentComment.actions'; import { actionTypes as editEventActionTypes } from '../../components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; +import { enrollmentEditEventActionTypes } from '../../components/Pages/EnrollmentEditEvent'; const initialReducerValue = {}; const { @@ -19,6 +20,17 @@ const { COMMIT_ENROLLMENT_EVENT_WITHOUT_ID, } = enrollmentSiteActionTypes; +const setAssignee = (state, action) => { + const { assignedUser, eventId } = action.payload; + + const events = state.enrollment.events.reduce( + (acc, e) => (e.event === eventId ? [...acc, { ...e, assignedUser }] : [...acc, e]), + [], + ); + + return { ...state, enrollment: { ...state.enrollment, events } }; +}; + export const enrollmentDomainDesc = createReducerDescription( { [COMMON_ENROLLMENT_SITE_DATA_SET]: (state, { payload: { enrollment, attributeValues } }) => ({ @@ -143,6 +155,8 @@ export const enrollmentDomainDesc = createReducerDescription( }); return { ...state, enrollment: { ...state.enrollment, events } }; }, + [enrollmentEditEventActionTypes.ASSIGNEE_SET]: setAssignee, + [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: setAssignee, }, 'enrollmentDomain', initialReducerValue, diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.js b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.js index a131dc4e35..1fd8c503e8 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.js +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.js @@ -25,6 +25,8 @@ import { workingListsCommonActionTypes } from '../../components/WorkingLists/Wor import type { Updaters } from '../../trackerRedux/trackerReducer'; import { registrationFormActionTypes } from '../../components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions'; import { enrollmentSiteActionTypes } from '../../components/Pages/common/EnrollmentOverviewDomain'; +import { enrollmentEditEventActionTypes } from '../../components/Pages/EnrollmentEditEvent'; +import { actionTypes as viewEventActionTypes } from '../../components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions'; function addErrorFeedback(state: ReduxState, message: string, action?: ?Node) { const newState = [...state]; @@ -119,5 +121,9 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback(state, i18n.t('Error editing the event, the changes made were not saved')), [enrollmentSiteActionTypes.ERROR_ENROLLMENT]: (state, action) => addErrorFeedback(state, i18n.t(action.payload.message)), + [viewEventActionTypes.ASSIGNEE_SAVE_FAILED]: state => + addErrorFeedback(state, i18n.t('Error updating the Assignee')), + [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: state => + addErrorFeedback(state, i18n.t('Error updating the Assignee')), }, 'feedbacks', []); diff --git a/src/core_modules/capture-core/reducers/descriptions/viewEvent.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/viewEvent.reducerDescription.js index ec4f51fe2b..f82ba16563 100644 --- a/src/core_modules/capture-core/reducers/descriptions/viewEvent.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/viewEvent.reducerDescription.js @@ -12,11 +12,35 @@ import { actionTypes as editEventDataEntryActionTypes, } from '../../components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; import { actionTypes as viewEventNotesActionTypes } from '../../components/Pages/ViewEvent/Notes/viewEventNotes.actions'; -import { assigneeSectionActionTypes } from '../../components/Pages/ViewEvent/RightColumn/AssigneeSection'; import { eventWorkingListsActionTypes } from '../../components/WorkingLists/EventWorkingLists'; import { actionTypes as widgetEventEditActionTypes, } from '../../components/WidgetEventEdit/WidgetEventEdit.actions'; +import { enrollmentEditEventActionTypes } from '../../components/Pages/EnrollmentEditEvent'; + +const setAssignee = (state, action) => { + const { assignee, eventId } = action.payload; + if (eventId !== state.eventId) { + return state; + } + + const newState = { + ...state, + saveInProgress: true, + loadedValues: { + ...state.loadedValues, + eventContainer: { + ...state.loadedValues.eventContainer, + event: { + ...state.loadedValues.eventContainer.event, + assignee, + }, + }, + }, + }; + + return newState; +}; export const viewEventPageDesc = createReducerDescription({ [viewEventActionTypes.VIEW_EVENT_FROM_URL]: (state, action) => { @@ -144,45 +168,8 @@ export const viewEventPageDesc = createReducerDescription({ eventHasChanged: true, }; }, - [assigneeSectionActionTypes.VIEW_EVENT_ASSIGNEE_SET]: (state, action) => { - const { assignee } = action.payload; - - const newState = { - ...state, - saveInProgress: true, - loadedValues: { - ...state.loadedValues, - eventContainer: { - ...state.loadedValues.eventContainer, - event: { - ...state.loadedValues.eventContainer.event, - assignee, - }, - }, - }, - }; - - return newState; - }, - [assigneeSectionActionTypes.VIEW_EVENT_ASSIGNEE_SAVE_COMPLETED]: (state, action) => { - if (action.meta.eventId !== state.eventId) { - return state; - } - - return { - ...state, - saveInProgress: false, - eventHasChanged: true, - }; - }, - [assigneeSectionActionTypes.VIEW_EVENT_ASSIGNEE_SAVE_FAILED]: (state, action) => { - if (action.meta.eventId !== state.eventId) { - return state; - } - - return { - ...state, - saveInProgress: false, - }; - }, + [viewEventActionTypes.ASSIGNEE_SET]: setAssignee, + [viewEventActionTypes.ASSIGNEE_SAVE_FAILED]: setAssignee, + [enrollmentEditEventActionTypes.ASSIGNEE_SET]: setAssignee, + [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: setAssignee, }, 'viewEventPage'); diff --git a/src/epics/trackerCapture.epics.js b/src/epics/trackerCapture.epics.js index bcb9aea9b3..fb5aec0812 100644 --- a/src/epics/trackerCapture.epics.js +++ b/src/epics/trackerCapture.epics.js @@ -135,7 +135,6 @@ import { runRulesOnEnrollmentFieldUpdateEpic, runRulesOnEnrollmentDataEntryFieldUpdateEpic, } from 'capture-core/components/DataEntries'; -import { saveAssigneeEpic } from 'capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection'; import { triggerLoadCoreEpic, loadAppEpic } from '../components/AppStart'; @@ -309,7 +308,6 @@ export const epics = combineEpics( openNewRelationshipRegisterTeiEpic, loadSearchGroupDuplicatesForReviewEpic, teiForNewEventRelationshipSavedEpic, - saveAssigneeEpic, validateSelectionsBasedOnUrlUpdateEpic, getOrgUnitDataBasedOnUrlUpdateEpic, setOrgUnitDataEmptyBasedOnUrlUpdateEpic,