From 1e9b3f9cd479e4a3db49494ee540d2b9d44fc073 Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Thu, 19 Dec 2024 15:07:00 +0100 Subject: [PATCH 1/4] feat: show related stages widget in view event page --- i18n/en.pot | 16 ++- ...llmentWithFirstStageDataEntry.container.js | 4 +- .../getConvertedRelatedStageEvent.types.js | 4 +- .../EnrollmentEditEventPage.component.js | 2 + .../EnrollmentEditEventPage.container.js | 12 ++ .../EnrollmentEditEventPage.types.js | 1 + .../PageLayout/DefaultPageLayout.constants.js | 6 + .../DefaultEnrollmentLayout.types.js | 1 + .../LayoutComponentConfig.js | 30 +++++ .../Validated/Validated.component.js | 4 +- .../RelatedStagesActions.component.js | 76 +++++++---- .../RelatedStagesActions.container.js} | 26 ++-- .../RelatedStagesActions.types.js | 18 ++- .../RelatedStagesActions/index.js | 2 +- .../WidgetRelatedStages.container.js | 126 ++++++++++++++++++ .../WidgetRelatedStages.types.js | 11 +- .../WidgetRelatedStages/hooks/index.js | 7 + .../hooks/useAddEventWithRelationship.js | 61 +++++++++ .../hooks/useBuildRelatedStageEventPayload.js | 95 +++++++++++++ .../{ => hooks}/useRelatedStages.js | 10 +- .../components/WidgetRelatedStages/index.js | 3 +- .../WidgetTwoEventWorkspace/hooks/index.js | 2 + .../WidgetTwoEventWorkspace/index.js | 1 + 23 files changed, 458 insertions(+), 60 deletions(-) rename src/core_modules/capture-core/components/WidgetRelatedStages/{WidgetRelatedStages.component.js => RelatedStagesActions/RelatedStagesActions.container.js} (82%) create mode 100644 src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.js create mode 100644 src/core_modules/capture-core/components/WidgetRelatedStages/hooks/index.js create mode 100644 src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.js create mode 100644 src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useBuildRelatedStageEventPayload.js rename src/core_modules/capture-core/components/WidgetRelatedStages/{ => hooks}/useRelatedStages.js (90%) diff --git a/i18n/en.pot b/i18n/en.pot index d4d4fb3813..88458264ab 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-12-05T11:39:04.447Z\n" -"PO-Revision-Date: 2024-12-05T11:39:04.447Z\n" +"POT-Creation-Date: 2024-12-11T08:18:31.184Z\n" +"PO-Revision-Date: 2024-12-11T08:18:31.184Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1397,12 +1397,21 @@ msgstr "Actions - {{relationshipName}}" msgid "Ambiguous relationships, contact system administrator" msgstr "Ambiguous relationships, contact system administrator" +msgid "Enter details" +msgstr "Enter details" + +msgid "Linked event" +msgstr "Linked event" + msgid "Enter details now" msgstr "Enter details now" msgid "Link to an existing event" msgstr "Link to an existing event" +msgid "An error occurred while linking the event" +msgstr "An error occurred while linking the event" + msgid "Scheduled date" msgstr "Scheduled date" @@ -1513,9 +1522,6 @@ msgstr "You do not have access to remove the link and delete the linked event" msgid "An error occurred while loading the widget." msgstr "An error occurred while loading the widget." -msgid "Linked event" -msgstr "Linked event" - msgid "" "This {{stageName}} event is linked to a {{linkedStageName}} event. Review " "the linked event details before entering data below" diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js index 420c663669..dcce7692a4 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js @@ -4,7 +4,7 @@ import type { Props } from './EnrollmentWithFirstStageDataEntry.types'; import { FirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry.component'; import { useDataEntrySections } from './hooks'; import { Section } from '../../../../metaData'; -import { WidgetRelatedStages } from '../../../WidgetRelatedStages'; +import { RelatedStagesActions } from '../../../WidgetRelatedStages'; const getSectionId = sectionId => (sectionId === Section.MAIN_SECTION_ID ? `${Section.MAIN_SECTION_ID}-stage` : sectionId); @@ -26,7 +26,7 @@ export const EnrollmentWithFirstStageDataEntry = (props: Props) => { firstStageMetaData={firstStageMetaData} dataEntrySections={dataEntrySections} /> - , - status?: string, + status: 'ACTIVE' | 'VISITED' | 'COMPLETED' | 'SCHEDULE' | 'OVERDUE' | 'SKIPPED', } export type RequestEvent = { ...CommonEventDetails, occurredAt: string, notes?: Array<{ value: string }>, - completedAt?: string, } export type LinkedRequestEvent = { ...CommonEventDetails, occurredAt?: string, - completedAt?: string, } export type ConvertedRelatedStageEventProps = {| 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 679bb616c4..4dd2ff5341 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 @@ -57,6 +57,7 @@ export const EnrollmentEditEventPageComponent = ({ onSaveAssigneeError, onDeleteTrackedEntitySuccess, onAccessLostFromTransfer, + onNavigateToEvent, }: PlainProps) => ( 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 f459cff54d..3d81fcba59 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 @@ -187,6 +187,17 @@ const EnrollmentEditEventPageWithContextPlain = ({ const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); + const onNavigateToEvent = (eventIdToRedirectTo: string) => { + history.push( + `/enrollmentEventEdit?${buildUrlQueryString({ + eventId: eventIdToRedirectTo, + orgUnitId, + programId, + enrollmentId, + })}`, + ); + }; + const onHandleScheduleSave = (eventData: Object) => { dispatch(updateEnrollmentEvent(eventId, eventData)); history.push(`enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -291,6 +302,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ onSaveAssigneeError={onSaveAssigneeError} events={enrollmentSite?.events} onAccessLostFromTransfer={onAccessLostFromTransfer} + onNavigateToEvent={onNavigateToEvent} /> ); }; 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 56e2d5c5de..a32173f2c2 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 @@ -29,6 +29,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, + onNavigateToEvent: (eventId: string) => void, onBackToMainPage: () => void, onBackToDashboard: () => void, onBackToViewEvent: () => void, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js index 0a8065a405..9e1486eae8 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js @@ -10,11 +10,13 @@ import { AssigneeWidget, WidgetTypes, TwoEventWorkspace, + WidgetRelatedStagesWorkspace, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; export const WidgetsForEnrollmentEventEdit: $ReadOnly<{ [key: string]: WidgetConfig }> = Object.freeze({ EditEventWorkspace, TwoEventWorkspace, + WidgetRelatedStagesWorkspace, EventNote, AssigneeWidget, ...DefaultWidgetsForEnrollmentOverview, @@ -26,6 +28,10 @@ export const DefaultPageLayout: PageLayoutConfig = { type: WidgetTypes.COMPONENT, name: 'EditEventWorkspace', }, + { + type: WidgetTypes.COMPONENT, + name: 'WidgetRelatedStagesWorkspace', + }, ], rightColumn: [ { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js index cbb1bea87a..b37abd6507 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js @@ -8,6 +8,7 @@ type DefaultComponents = 'QuickActions' | 'AssigneeWidget' | 'NewEventWorkspace' | 'EditEventWorkspace' + | 'WidgetRelatedStagesWorkspace' | 'EnrollmentNote' | 'EventNote' | 'TrackedEntityRelationship' diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js index 2baee53485..1bf2275e72 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js @@ -29,6 +29,10 @@ import type { InputIndicatorProps, } from '../../../../../WidgetFeedback/WidgetFeedback.types'; import { WidgetTwoEventWorkspace } from '../../../../../WidgetTwoEventWorkspace'; +import { WidgetRelatedStages } from '../../../../../WidgetRelatedStages'; +import { + EnrollmentPageKeys, +} from '../DefaultEnrollmentLayout.constants'; export const QuickActions: WidgetConfig = { Component: EnrollmentQuickActions, @@ -286,3 +290,29 @@ export const EventNote: WidgetConfig = { dataEntryId, }), }; + +export const WidgetRelatedStagesWorkspace: WidgetConfig = { + Component: WidgetRelatedStages, + shouldHideWidget: ({ currentPage }) => currentPage === EnrollmentPageKeys.EDIT_EVENT, + getProps: ({ + program, + stageId, + enrollmentId, + eventId, + teiId, + onUpdateEnrollmentStatus, + onUpdateEnrollmentStatusSuccess, + onUpdateEnrollmentStatusError, + onNavigateToEvent, + }) => ({ + programId: program.id, + programStageId: stageId, + enrollmentId, + eventId, + teiId, + onUpdateEnrollment: onUpdateEnrollmentStatus, + onUpdateEnrollmentSuccess: onUpdateEnrollmentStatusSuccess, + onUpdateEnrollmentError: onUpdateEnrollmentStatusError, + onNavigateToEvent, + }), +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js index 663348da1b..7f34363548 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js @@ -6,7 +6,7 @@ import { Widget } from '../../Widget'; import { DataEntry } from '../DataEntry'; import { FinishButtons } from '../FinishButtons'; import { SavingText } from '../SavingText'; -import { WidgetRelatedStages } from '../../WidgetRelatedStages'; +import { RelatedStagesActions } from '../../WidgetRelatedStages'; import type { Props } from './validated.types'; const styles = () => ({ @@ -47,7 +47,7 @@ const ValidatedPlain = ({ id={id} orgUnit={orgUnit} /> - ({ clearSelections: { padding: spacers.dp8, }, + link: { + padding: spacers.dp8, + }, }); const Schedule = ({ @@ -171,6 +174,16 @@ const LinkExistingResponse = ({ ); }; +const LinkButton = withStyles(styles)(({ onLink, label, loading, classes }) => ( + onLink ? ( +
+ +
+ ) : null +)); + const RelatedStagesActionsPlain = ({ classes, type, @@ -184,7 +197,8 @@ const RelatedStagesActionsPlain = ({ errorMessages, saveAttempted, actionsOptions, -}: Props) => { + onLink, +}: PlainProps) => { const { programStage } = useProgramStageInfo(constraint?.programStage?.id); const selectedAction = useMemo(() => relatedStagesDataValues.linkMode, [relatedStagesDataValues.linkMode]); @@ -253,37 +267,47 @@ const RelatedStagesActionsPlain = ({ )} {selectedAction === relatedStageActions.SCHEDULE_IN_ORG && ( - + <> + + + )} {selectedAction === relatedStageActions.ENTER_DATA && ( - + <> + + + )} {selectedAction === relatedStageActions.LINK_EXISTING_RESPONSE && ( - + <> + + + )} + ); }; -export const RelatedStagesActions: ComponentType<$Diff> = withStyles(styles)(RelatedStagesActionsPlain); +export const RelatedStagesActions: ComponentType<$Diff> = withStyles(styles)(RelatedStagesActionsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.js similarity index 82% rename from src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js rename to src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.js index 9ce0628481..e50803fb6f 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.component.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.js @@ -1,19 +1,18 @@ // @flow import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; -import { useRelatedStages } from './useRelatedStages'; -import { useOrgUnitAutoSelect } from '../../dataQueries'; -import type { Props, RelatedStageDataValueStates } from './WidgetRelatedStages.types'; -import type { ErrorMessagesForRelatedStages } from './RelatedStagesActions'; -import { RelatedStagesActions } from './RelatedStagesActions'; -import { relatedStageStatus } from './constants'; -import { useStageLabels } from './hooks/useStageLabels'; -import { relatedStageWidgetIsValid } from './relatedStageEventIsValid/relatedStageEventIsValid'; -import { useRelatedStageEvents } from './hooks/useRelatedStageEvents'; +import { useOrgUnitAutoSelect } from '../../../dataQueries'; +import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; +import type { Props, ErrorMessagesForRelatedStages } from './RelatedStagesActions.types'; +import { RelatedStagesActions as RelatedStagesActionsComponent } from './RelatedStagesActions.component'; +import { relatedStageStatus } from '../constants'; +import { useStageLabels, useRelatedStageEvents, useRelatedStages } from '../hooks'; +import { relatedStageWidgetIsValid } from '../relatedStageEventIsValid/relatedStageEventIsValid'; -const WidgetRelatedStagesPlain = ({ +const RelatedStagesActionsPlain = ({ programId, enrollmentId, programStageId, + onLink, ...passOnProps }: Props, ref) => { const { currentRelatedStagesStatus, selectedRelationshipType, constraint } = useRelatedStages({ @@ -97,7 +96,7 @@ const WidgetRelatedStagesPlain = ({ } return ( - ); }; -export const WidgetRelatedStages = forwardRef < Props, {| +export const RelatedStagesActions = forwardRef < Props, {| eventHasLinkableStageRelationship: Function, formIsValidOnSave: Function, getLinkedStageValues: Function - |}>(WidgetRelatedStagesPlain); + |}>(RelatedStagesActionsPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.types.js b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.types.js index 18aedce6ac..3dfe97c009 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.types.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.types.js @@ -25,7 +25,7 @@ export type RelatedStagesEvents = { status: string, } -export type Props = {| +export type PlainProps = {| type: string, relationshipName: string, relatedStagesDataValues: RelatedStageDataValueStates, @@ -37,6 +37,7 @@ export type Props = {| constraint: ?Constraint, addErrorMessage: (ErrorMessagesForRelatedStages) => void, setRelatedStagesDataValues: (() => Object) => void, + onLink?: () => void, actionsOptions?: { [key: $Keys]: { hidden?: boolean, @@ -46,3 +47,18 @@ export type Props = {| }, ...CssClasses |} + +export type Props = {| + programId: string, + enrollmentId?: string, + programStageId: string, + onLink?: () => void, + actionsOptions?: { + [key: $Keys]: { + hidden?: boolean, + disabled?: boolean, + disabledMessage?: string + }, + }, +|} + diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/index.js b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/index.js index 9655a91302..7d46218f32 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/index.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/index.js @@ -1,3 +1,3 @@ // @flow -export { RelatedStagesActions } from './RelatedStagesActions.component'; +export { RelatedStagesActions } from './RelatedStagesActions.container'; export type { ErrorMessagesForRelatedStages } from './RelatedStagesActions.types'; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.js b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.js new file mode 100644 index 0000000000..16144ab6db --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.js @@ -0,0 +1,126 @@ +// @flow +import React, { type ComponentType, useRef, useCallback } from 'react'; +import { IconLink24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core'; +import i18n from '@dhis2/d2-i18n'; +import { Widget } from '../Widget'; +import { type RelatedStageRefPayload } from './index'; +import { RelatedStagesActions } from './RelatedStagesActions'; +import { useLinkedEventByOriginId } from '../WidgetTwoEventWorkspace/hooks'; +import type { Props } from './WidgetRelatedStages.types'; +import { + useRelatedStages, + useBuildRelatedStageEventPayload, + useAddEventWithRelationship, + createServerData, +} from './hooks'; +import { relatedStageStatus } from './constants'; +import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; +import { type RequestEvent } from '../DataEntries'; + +const styles = { + header: { + display: 'flex', + alignItems: 'center', + }, + icon: { + paddingRight: spacers.dp8, + }, + actions: { + margin: `0 ${spacers.dp16} ${spacers.dp16} ${spacers.dp16}`, + }, +}; + +export const WidgetRelatedStagesPlain = ({ + programId, + eventId, + enrollmentId, + programStageId, + teiId, + actionsOptions, + onUpdateEnrollment, + onUpdateEnrollmentSuccess, + onUpdateEnrollmentError, + onNavigateToEvent, + classes, +}: Props) => { + const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); + const { currentRelatedStagesStatus } = useRelatedStages({ programStageId, programId }); + const { + linkedEvent, + isLoading: isLinkedEventLoading, + } = useLinkedEventByOriginId({ originEventId: eventId }); + const relatedStageRef = useRef(null); + const { buildRelatedStageEventPayload } = useBuildRelatedStageEventPayload(); + const { addEventWithRelationship } = useAddEventWithRelationship({ + eventId, + onUpdateEnrollment, + onUpdateEnrollmentSuccess, + onUpdateEnrollmentError, + onNavigateToEvent, + }); + + const onLink = useCallback(() => { + // $FlowFixMe[incompatible-type] + const serverRequestEvent: ?RequestEvent = enrollment?.events.find(e => e.event === eventId); + + const { + formHasError, + linkedEvent: relatedStageLinkedEvent, + relationship, + linkMode, + } = buildRelatedStageEventPayload({ + serverRequestEvent, + relatedStageRef, + programStageId, + programId, + teiId, + enrollmentId, + }); + + if (!formHasError && relationship && linkMode) { + const serverData = createServerData({ enrollment, linkedEvent: relatedStageLinkedEvent, relationship }); + addEventWithRelationship({ serverData, linkMode, eventIdToRedirectTo: relatedStageLinkedEvent?.event }); + } + }, [ + programStageId, + programId, + teiId, + eventId, + enrollmentId, + enrollment, + buildRelatedStageEventPayload, + addEventWithRelationship, + ]); + + if (isLinkedEventLoading || linkedEvent || currentRelatedStagesStatus !== relatedStageStatus.LINKABLE) { + return null; + } + + return ( + + + + + {i18n.t('Linked event')} + + } + > +
+ +
+
+ ); +}; + +export const WidgetRelatedStages: ComponentType = withStyles(styles)(WidgetRelatedStagesPlain); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js index 7876e528c1..9288f6f008 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.types.js @@ -17,8 +17,14 @@ export type RelationshipType = {| export type Props = {| programId: string, - enrollmentId?: string, + eventId: string, + teiId: string, + enrollmentId: string, programStageId: string, + onUpdateEnrollment: (enrollment: Object) => void, + onUpdateEnrollmentSuccess: ({ redirect?: boolean }) => void, + onUpdateEnrollmentError: (message: string) => void, + onNavigateToEvent: (eventId: string) => void, actionsOptions?: { [key: $Keys]: { hidden?: boolean, @@ -26,7 +32,9 @@ export type Props = {| disabledMessage?: string }, }, + ...CssClasses, |} + export type RelatedStageDataValueStates = {| linkMode: ?$Keys, scheduledAt: string, @@ -41,6 +49,7 @@ export type RelatedStageDataValueStates = {| export type RelatedStageRelationshipType = {| id: string, + displayName: string, fromConstraint: {| programStage: { id: string, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/index.js b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/index.js new file mode 100644 index 0000000000..54d9aa0537 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/index.js @@ -0,0 +1,7 @@ +// @flow +export { useRelatedStages } from './useRelatedStages'; +export { useStageLabels } from './useStageLabels'; +export { useRelatedStageEvents } from './useRelatedStageEvents'; +export { useCanAddNewEventToStage } from './useCanAddNewEventToStage'; +export { useBuildRelatedStageEventPayload, createServerData } from './useBuildRelatedStageEventPayload'; +export { useAddEventWithRelationship } from './useAddEventWithRelationship'; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.js b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.js new file mode 100644 index 0000000000..39230785ff --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.js @@ -0,0 +1,61 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { useDataEngine } from '@dhis2/app-runtime'; +import { useMutation, useQueryClient } from 'react-query'; +import { relatedStageActions } from '../constants'; + +const ReactQueryAppNamespace = 'capture'; + +const addEventWithRelationshipMutation = { + resource: '/tracker?async=false&importStrategy=CREATE_AND_UPDATE', + type: 'create', + data: ({ serverData }) => serverData, +}; + +export const useAddEventWithRelationship = ({ + eventId, + onUpdateEnrollment, + onUpdateEnrollmentSuccess, + onUpdateEnrollmentError, + onNavigateToEvent, +}: { + eventId: string, + onUpdateEnrollment: (enrollment: Object) => void, + onUpdateEnrollmentSuccess: ({ redirect?: boolean }) => void, + onUpdateEnrollmentError: (message: string) => void, + onNavigateToEvent: (eventId: string) => void, +}) => { + const dataEngine = useDataEngine(); + const queryClient = useQueryClient(); + + const { mutate } = useMutation( + ({ serverData }: Object) => + dataEngine.mutate(addEventWithRelationshipMutation, { + variables: { + serverData, + }, + }), + { + onMutate: (payload: { serverData: Object }) => { + const enrollmentToUpdate = payload.serverData.enrollments?.[0]; + enrollmentToUpdate && onUpdateEnrollment(enrollmentToUpdate); + }, + onSuccess: (_, payload: { linkMode: string, eventIdToRedirectTo?: string }) => { + const queryKey = [ReactQueryAppNamespace, 'linkedEventByOriginEvent', eventId]; + queryClient.refetchQueries(queryKey); + onUpdateEnrollmentSuccess({}); + + if (payload.linkMode === relatedStageActions.ENTER_DATA && payload.eventIdToRedirectTo) { + onNavigateToEvent(payload.eventIdToRedirectTo); + } + }, + onError: () => { + onUpdateEnrollmentError(i18n.t('An error occurred while linking the event')); + }, + }, + ); + + return { + addEventWithRelationship: mutate, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useBuildRelatedStageEventPayload.js b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useBuildRelatedStageEventPayload.js new file mode 100644 index 0000000000..f235cd07fb --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useBuildRelatedStageEventPayload.js @@ -0,0 +1,95 @@ +// @flow +import { getConvertedRelatedStageEvent, type RequestEvent, type LinkedRequestEvent } from '../../DataEntries'; +import type { RelatedStageRefPayload } from '../index'; + +export const createServerData = ({ + linkedEvent, + relationship, + enrollment, +}: { + linkedEvent: ?LinkedRequestEvent, + relationship: ?Object, + enrollment: Object, +}) => { + const updatedEnrollment = { ...enrollment, events: [...enrollment.events, linkedEvent] }; + if (linkedEvent) { + return { + enrollments: [updatedEnrollment], + relationships: [relationship], + }; + } + return { + relationships: [relationship], + }; +}; + +export const useBuildRelatedStageEventPayload = () => { + const buildRelatedStageEventPayload = ({ + serverRequestEvent, + relatedStageRef, + programStageId, + programId, + teiId, + enrollmentId, + }: { + serverRequestEvent: ?RequestEvent, + relatedStageRef?: { current: ?RelatedStageRefPayload }, + programStageId: string, + programId: string, + teiId: string, + enrollmentId: string, + }) => { + if (relatedStageRef?.current && relatedStageRef.current.eventHasLinkableStageRelationship()) { + const isValid = relatedStageRef.current.formIsValidOnSave(); + + if (!isValid || !relatedStageRef.current?.getLinkedStageValues || !programStageId || !serverRequestEvent) { + return { + formHasError: true, + linkedEvent: null, + relationship: null, + linkMode: null, + }; + } + + const { selectedRelationshipType, relatedStageDataValues, linkMode } = + relatedStageRef.current.getLinkedStageValues(); + + if (!linkMode) { + return { + formHasError: false, + linkedEvent: null, + relationship: null, + linkMode: null, + }; + } + + const { linkedEvent, relationship } = getConvertedRelatedStageEvent({ + linkMode, + relatedStageDataValues, + serverRequestEvent, + relatedStageType: selectedRelationshipType, + programId, + currentProgramStageId: programStageId, + teiId, + enrollmentId, + }); + + return { + formHasError: false, + linkedEvent, + relationship, + linkMode, + }; + } + return { + formHasError: false, + linkedEvent: null, + relationship: null, + linkMode: null, + }; + }; + + return { + buildRelatedStageEventPayload, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/useRelatedStages.js b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useRelatedStages.js similarity index 90% rename from src/core_modules/capture-core/components/WidgetRelatedStages/useRelatedStages.js rename to src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useRelatedStages.js index 6472c0b801..6622c8df31 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/useRelatedStages.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useRelatedStages.js @@ -1,10 +1,10 @@ // @flow import { useMemo } from 'react'; -import { relatedStageStatus } from './constants'; -import { getUserStorageController, userStores } from '../../storageControllers'; -import { useIndexedDBQuery } from '../../utils/reactQueryHelpers'; -import { RELATIONSHIP_ENTITIES } from './WidgetRelatedStages.constants'; -import type { RelationshipType } from './WidgetRelatedStages.types'; +import { relatedStageStatus } from '../constants'; +import { getUserStorageController, userStores } from '../../../storageControllers'; +import { useIndexedDBQuery } from '../../../utils/reactQueryHelpers'; +import { RELATIONSHIP_ENTITIES } from '../WidgetRelatedStages.constants'; +import type { RelationshipType } from '../WidgetRelatedStages.types'; const getRelationshipTypeFromIndexedDB = () => { const storageController = getUserStorageController(); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/index.js b/src/core_modules/capture-core/components/WidgetRelatedStages/index.js index e75b93c8cc..b23f2475ec 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/index.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/index.js @@ -1,5 +1,6 @@ // @flow -export { WidgetRelatedStages } from './WidgetRelatedStages.component'; +export { RelatedStagesActions } from './RelatedStagesActions'; +export { WidgetRelatedStages } from './WidgetRelatedStages.container'; export type { RelatedStageDataValueStates, RelatedStageRefPayload, diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/index.js b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/index.js index 195e176eef..0596291a5b 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/index.js +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/hooks/index.js @@ -1,3 +1,5 @@ +// @flow + export { useClientDataValues } from './useClientDataValues'; export { useLinkedEventByOriginId } from './useLinkedEventByOriginId'; export { useRelationshipTypeAccess } from './useRelationshipTypeAccess'; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/index.js b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/index.js index b057d60b99..69e57db145 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/index.js +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/index.js @@ -2,3 +2,4 @@ export { WidgetTwoEventWorkspace } from './WidgetTwoEventWorkspace.container'; export { WidgetTwoEventWorkspaceWrapperTypes } from './WidgetTwoEventWorkspaceWrapper.const'; +export { useLinkedEventByOriginId } from './hooks'; From c61951191251dbe707a80242b0f671fb665a315a Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Mon, 6 Jan 2025 17:11:18 +0100 Subject: [PATCH 2/4] feat: show related stages widget in view event page --- .../EnrollmentRegistrationEntry.types.js | 9 ++--- .../RelatedStagesActions.component.js | 37 +++++++++++++++---- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js index 6eff575ea5..e0db0b6a92 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js @@ -1,12 +1,11 @@ // @flow import type { Node } from 'react'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; -import type { RegistrationFormMetadata } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm/types'; import type { RenderCustomCardActions } from '../../CardList'; import type { SaveForDuplicateCheck } from '../common/TEIAndEnrollment/DuplicateCheckOnSave'; import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMessagePostProcessor'; import type { InputAttribute } from './hooks/useFormValues'; -import { RenderFoundation, ProgramStage } from '../../../metaData'; +import { RenderFoundation, ProgramStage, Enrollment } from '../../../metaData'; import type { RelatedStageRefPayload } from '../../WidgetRelatedStages'; import { relatedStageActions } from '../../WidgetRelatedStages'; @@ -67,7 +66,7 @@ export type OwnProps = $ReadOnly<{| skipDuplicateCheck?: ?boolean, trackedEntityInstanceAttributes?: Array, saveButtonText: (trackedEntityName: string) => string, - firstStageMetaData?: ?{ stage: ?ProgramStage }, + firstStageMetaData?: ?{ stage: ProgramStage }, relatedStageRef?: { current: ?RelatedStageRefPayload }, relatedStageActionsOptions?: { [key: $Keys]: { @@ -85,8 +84,8 @@ type ContainerProps = {| onCancel: () => void, isUserInteractionInProgress: boolean, isSavingInProgress: boolean, - enrollmentMetadata: RegistrationFormMetadata, - formFoundation: ?RenderFoundation, + enrollmentMetadata: Enrollment, + formFoundation: RenderFoundation, formId: ?string, saveButtonText: string, |}; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.js b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.js index af35348fc9..0c73a5a731 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.js +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.js @@ -174,15 +174,21 @@ const LinkExistingResponse = ({ ); }; -const LinkButton = withStyles(styles)(({ onLink, label, loading, classes }) => ( - onLink ? ( +const LinkButton = withStyles(styles)(({ onLink, label, saveAttempted, errorMessages, classes }) => { + if (!onLink) { + return null; + } + + const disabled = saveAttempted && Object.values(errorMessages).filter(Boolean).length !== 0; + + return (
-
- ) : null -)); + ); +}); const RelatedStagesActionsPlain = ({ classes, @@ -275,7 +281,12 @@ const RelatedStagesActionsPlain = ({ saveAttempted={saveAttempted} errorMessages={errorMessages} /> - + )} @@ -288,7 +299,12 @@ const RelatedStagesActionsPlain = ({ saveAttempted={saveAttempted} errorMessages={errorMessages} /> - + )} @@ -302,7 +318,12 @@ const RelatedStagesActionsPlain = ({ errorMessages={errorMessages} saveAttempted={saveAttempted} /> - + )} From 9d9879b47fc7ac8d1215dbf7033b12e1533d6eea Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Thu, 16 Jan 2025 10:47:42 +0100 Subject: [PATCH 3/4] fix: remove breaking tests --- .../WidgetsForEnrollmentAddEventPage.feature | 9 --------- .../WidgetsForEnrollmentEditEvent.feature | 13 +++---------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature index cd6062c7fe..f4dfa1cc08 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature @@ -38,15 +38,6 @@ Feature: The user interacts with the widgets on the enrollment add event page And the user sees the owner organisation unit And the user sees the last update date - Scenario: You can delete a tracked entity from the profile widget - Given you add a new tracked entity in the Malaria focus investigation program - When the user clicks the "Back to all stages and events" button - When the user clicks the "New Event" button - When you open the overflow menu and click the "Delete Focus area" button - Then you see the delete tracked entity confirmation modal - When you confirm by clicking the "Yes, delete Focus area" button - Then you are redirected to the home page - Scenario: User can open the delete modal Given you land on the enrollment add event page by having typed #/enrollmentEventNew?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=EaOyKGOIGRp&enrollmentId=wBU0RAsYjKE&stageId=A03MvHHogjR Then the enrollment widget should be opened diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature index defa075922..174e58dcc6 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature @@ -38,13 +38,6 @@ Feature: The user interacts with the widgets on the enrollment edit event And the user sees the owner organisation unit And the user sees the last update date - Scenario: You can delete a tracked entity from the profile widget - Given you add a new tracked entity in the Malaria focus investigation program - When you open the overflow menu and click the "Delete Focus area" button - Then you see the delete tracked entity confirmation modal - When you confirm by clicking the "Yes, delete Focus area" button - Then you are redirected to the home page - Scenario: User can open the delete modal Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 Then the enrollment widget should be opened @@ -76,7 +69,7 @@ Feature: The user interacts with the widgets on the enrollment edit event Then the event has the user Tracker demo User assigned When you remove the assigned user Then the event has no assignd user - + @v>=41 Scenario: The user can view an event changelog on the enrollment edit event Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8 @@ -85,7 +78,7 @@ Feature: The user interacts with the widgets on the enrollment edit event And the changelog modal should contain data # One row is filtered out as the metadata is no longer there And the number of changelog table rows should be 9 - + @v>=41 Scenario: The user can change changelog page size Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8 @@ -96,7 +89,7 @@ Feature: The user interacts with the widgets on the enrollment edit event And you change the page size to 100 Then the number of changelog table rows should be 37 Then the table footer should display page 1 - + @v>=41 Scenario: The user can move to the next page in the changelog Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8 From e1b9836475364d92f79b05eb2b4e03eb2e70fd05 Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Thu, 16 Jan 2025 10:58:55 +0100 Subject: [PATCH 4/4] Revert "fix: remove breaking tests" This reverts commit 9d9879b47fc7ac8d1215dbf7033b12e1533d6eea. --- .../WidgetsForEnrollmentAddEventPage.feature | 9 +++++++++ .../WidgetsForEnrollmentEditEvent.feature | 13 ++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature index f4dfa1cc08..cd6062c7fe 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/WidgetsForEnrollmentAddEventPage.feature @@ -38,6 +38,15 @@ Feature: The user interacts with the widgets on the enrollment add event page And the user sees the owner organisation unit And the user sees the last update date + Scenario: You can delete a tracked entity from the profile widget + Given you add a new tracked entity in the Malaria focus investigation program + When the user clicks the "Back to all stages and events" button + When the user clicks the "New Event" button + When you open the overflow menu and click the "Delete Focus area" button + Then you see the delete tracked entity confirmation modal + When you confirm by clicking the "Yes, delete Focus area" button + Then you are redirected to the home page + Scenario: User can open the delete modal Given you land on the enrollment add event page by having typed #/enrollmentEventNew?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8&teiId=EaOyKGOIGRp&enrollmentId=wBU0RAsYjKE&stageId=A03MvHHogjR Then the enrollment widget should be opened diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature index 174e58dcc6..defa075922 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/WidgetsForEnrollmentEditEvent.feature @@ -38,6 +38,13 @@ Feature: The user interacts with the widgets on the enrollment edit event And the user sees the owner organisation unit And the user sees the last update date + Scenario: You can delete a tracked entity from the profile widget + Given you add a new tracked entity in the Malaria focus investigation program + When you open the overflow menu and click the "Delete Focus area" button + Then you see the delete tracked entity confirmation modal + When you confirm by clicking the "Yes, delete Focus area" button + Then you are redirected to the home page + Scenario: User can open the delete modal Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 Then the enrollment widget should be opened @@ -69,7 +76,7 @@ Feature: The user interacts with the widgets on the enrollment edit event Then the event has the user Tracker demo User assigned When you remove the assigned user Then the event has no assignd user - + @v>=41 Scenario: The user can view an event changelog on the enrollment edit event Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8 @@ -78,7 +85,7 @@ Feature: The user interacts with the widgets on the enrollment edit event And the changelog modal should contain data # One row is filtered out as the metadata is no longer there And the number of changelog table rows should be 9 - + @v>=41 Scenario: The user can change changelog page size Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8 @@ -89,7 +96,7 @@ Feature: The user interacts with the widgets on the enrollment edit event And you change the page size to 100 Then the number of changelog table rows should be 37 Then the table footer should display page 1 - + @v>=41 Scenario: The user can move to the next page in the changelog Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=QsAhMiZtnl2&orgUnitId=DiszpKrYNg8