diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b8e29d4b..ddcfc2c8a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [100.41.2](https://github.com/dhis2/capture-app/compare/v100.41.1...v100.41.2) (2023-10-12) + + +### Bug Fixes + +* [DHIS2-15827] remove effects of 'Prevent adding new events to stage' action from the form ([#3418](https://github.com/dhis2/capture-app/issues/3418)) ([f17d087](https://github.com/dhis2/capture-app/commit/f17d087f368cf4d6b26923fd5cf4fb15d7795ba2)) + +## [100.41.1](https://github.com/dhis2/capture-app/compare/v100.41.0...v100.41.1) (2023-10-09) + + +### Bug Fixes + +* [DHIS2-15734] assign effect dynamic formId object key ([#3422](https://github.com/dhis2/capture-app/issues/3422)) ([85f242b](https://github.com/dhis2/capture-app/commit/85f242b2b0c6092b46e92f087e21e102a2e1e19d)) + # [100.41.0](https://github.com/dhis2/capture-app/compare/v100.40.1...v100.41.0) (2023-10-03) diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage.feature b/cypress/integration/EnrollmentPage/HiddenProgramStage.feature index 4e6e427e3e..e13095474a 100644 --- a/cypress/integration/EnrollmentPage/HiddenProgramStage.feature +++ b/cypress/integration/EnrollmentPage/HiddenProgramStage.feature @@ -3,5 +3,4 @@ Feature: Hidden program stage Scenario: The user cannot add an event in a hidden program stage Given you add an enrollment event that will result in a rule effect to hide a program stage Then the New Postpartum care visit event button is disabled in the stages and events widget - And and an error is show in the Postpartum care visit stage And the Postpartum care visit button is disabled in the enrollmentEventNew page diff --git a/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js b/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js index edf6b833bd..f9e5cf69c2 100644 --- a/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js +++ b/cypress/integration/EnrollmentPage/HiddenProgramStage/index.js @@ -45,18 +45,6 @@ Then('the New Postpartum care visit event button is disabled in the stages and e .should('be.disabled'); }); -Then('and an error is show in the Postpartum care visit stage', () => { - cy.visit( - '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA&stageId=bbKtnxRZKEP', - ); - cy.contains('[data-test="dhis2-uicore-button"]', 'Complete') - .should('be.disabled'); - cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing') - .should('be.disabled'); - cy.contains('[data-test="dhis2-uicore-noticebox-content"]', 'You can\'t add any more Postpartum care visit events') - .should('exist'); -}); - Then('the Postpartum care visit button is disabled in the enrollmentEventNew page', () => { cy.visit( '/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA', diff --git a/docs/user/resources/images/first-stage-during-registration.png b/docs/user/resources/images/first-stage-during-registration.png new file mode 100644 index 0000000000..1baba4bc34 Binary files /dev/null and b/docs/user/resources/images/first-stage-during-registration.png differ diff --git a/docs/user/using-the-capture-app.md b/docs/user/using-the-capture-app.md index 1de82958d9..b335b3e7ce 100644 --- a/docs/user/using-the-capture-app.md +++ b/docs/user/using-the-capture-app.md @@ -203,8 +203,12 @@ You can set multiple program stages within a program to be auto-generating (this A program can be configured to automatically take the user to register a new event immediately after enrolling a tracked entity instance. To enable this behavior, the program must have at least one program stage with the "Open data entry form after registration" option checked. If more than one program stage has this option enabled, the first stage will be used. To configure it, you must follow the steps described in the [Enrollment with auto generated events](#enrollment-with-auto-generated-events) section and then check the option "Open data entry form after enrollment". - -![](resources/images/open-data-entry-form-after-enrollment.png) + +### Enrollment with first stage on registration page + +For tracker programs enable the "First stage appears on registration page" flag in the Maintenance. The enrollment registration page will now display the first program stage the user has access to. + +![](resources/images/first-stage-during-registration.png) #### Active type of event diff --git a/package.json b/package.json index fbf2ef4982..64230da5bf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.41.0", + "version": "100.41.2", "cacheVersion": "6", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.41.0", + "@dhis2/rules-engine-javascript": "100.41.2", "@dhis2/app-runtime": "^3.9.3", "@dhis2/d2-i18n": "^1.1.0", "@dhis2/d2-icons": "^1.0.1", diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 411dcf5450..88cc2da714 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.41.0", + "version": "100.41.2", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js index a439eee13d..e16304cf87 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js @@ -7,6 +7,9 @@ import type { OwnProps } from './EnrollmentRegistrationEntry.types'; import { useLifecycle } from './hooks'; import { useRulesEngineOrgUnit } from '../../../hooks'; import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; +import { + useBuildEnrollmentPayload, +} from './hooks/useBuildEnrollmentPayload'; export const EnrollmentRegistrationEntry: ComponentType = ({ selectedScopeId, @@ -26,7 +29,14 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ formId, enrollmentMetadata, formFoundation, - } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); + } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit, teiId, selectedScopeId); + const { buildTeiWithEnrollment } = useBuildEnrollmentPayload({ + programId: selectedScopeId, + dataEntryId: id, + orgUnitId, + teiId, + trackedEntityTypeId: enrollmentMetadata?.trackedEntityType?.id, + }); const isUserInteractionInProgress: boolean = useSelector( state => @@ -40,10 +50,16 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ const isSavingInProgress = useSelector(({ possibleDuplicates, newPage }) => possibleDuplicates.isLoading || possibleDuplicates.isUpdating || !!newPage.uid); + if (error) { return error.errorComponent; } + const onSaveWithEnrollment = () => { + const teiWithEnrollment = buildTeiWithEnrollment(); + onSave(teiWithEnrollment); + }; + return ( = ({ orgUnit={orgUnit} isUserInteractionInProgress={isUserInteractionInProgress} isSavingInProgress={isSavingInProgress} - onSave={() => onSave(formFoundation, firstStageMetaData)} + onSave={onSaveWithEnrollment} /> ); }; 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 bda753f1ba..d58a42aff2 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 @@ -8,6 +8,29 @@ import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMess import type { InputAttribute } from './hooks/useFormValues'; import { RenderFoundation, ProgramStage } from '../../../metaData'; +export type EnrollmentPayload = {| + trackedEntity: string, + trackedEntityType: string, + orgUnit: string, + geometry: any, + enrollments: [ + {| + occurredAt: string, + orgUnit: string, + program: string, + status: string, + enrolledAt: string, + events: Array<{ + orgUnit: string, + }>, + attributes: Array<{ + attribute: string, + value: any, + }>, + |} + ] +|} + export type OwnProps = $ReadOnly<{| id: string, orgUnitId: string, diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js new file mode 100644 index 0000000000..95778162b0 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js @@ -0,0 +1,188 @@ +// @flow +import { useSelector } from 'react-redux'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import { + getTrackerProgramThrowIfNotFound, + Section, +} from '../../../../metaData'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { + convertDataEntryValuesToClientValues, +} from '../../../DataEntry/common/convertDataEntryValuesToClientValues'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { + useBuildFirstStageRegistration, +} from './useBuildFirstStageRegistration'; +import { + useMetadataForRegistrationForm, +} from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { + useMergeFormFoundationsIfApplicable, +} from './useMergeFormFoundationsIfApplicable'; +import { + deriveAutoGenerateEvents, + deriveFirstStageDuringRegistrationEvent, +} from '../../../Pages/New/RegistrationDataEntry/helpers'; +import { FEATURETYPE } from '../../../../constants'; +import type { EnrollmentPayload } from '../EnrollmentRegistrationEntry.types'; + +type DataEntryReduxConverterProps = { + programId: string; + dataEntryId: string; + itemId?: string; + orgUnitId: string; + teiId: ?string; + trackedEntityTypeId: string; +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + const clientValues = formFoundation.convertValues(formValues, convertFormToClient); + return clientValues; +} + +function getServerValuesForMainValues( + values: Object, + meta: Object, + formFoundation: RenderFoundation, +) { + const clientValues = convertDataEntryValuesToClientValues( + values, + meta, + formFoundation, + ) || {}; + + // potientally run this through a server to client converter for enrollment, the same way as for event + const serverValues = Object + .keys(clientValues) + .reduce((acc, key) => { + const value = clientValues[key]; + const type = meta[key].type; + acc[key] = convertClientToServer(value, type); + return acc; + }, {}); + + return serverValues; +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); + +const deriveAttributesFromFormValues = (formValues = {}) => + Object.keys(formValues) + .filter(key => !geometryType(key)) + .map<{ attribute: string, value: ?any }>(key => ({ attribute: key, value: formValues[key] })); + +export const useBuildEnrollmentPayload = ({ + programId, + dataEntryId, + itemId = 'newEnrollment', + orgUnitId, + teiId, + trackedEntityTypeId, +}: DataEntryReduxConverterProps) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); + const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); + const { formFoundation: scopeFormFoundation } = useMetadataForRegistrationForm({ selectedScopeId: programId }); + const { firstStageMetaData } = useBuildFirstStageRegistration(programId); + const { formFoundation } = useMergeFormFoundationsIfApplicable(scopeFormFoundation, firstStageMetaData); + + const buildTeiWithEnrollment = (): EnrollmentPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const firstStage = firstStageMetaData && firstStageMetaData.stage; + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertAndGroupBySection(clientValues, convertClientToServer); + const serverValuesForMainValues = getServerValuesForMainValues( + dataEntryFieldValues, + dataEntryFieldsMeta, + formFoundation, + ); + const { enrolledAt, occurredAt } = serverValuesForMainValues; + + const { stages } = getTrackerProgramThrowIfNotFound(programId); + + const attributeCategoryOptionsId = 'attributeCategoryOptions'; + const attributeCategoryOptions = Object.keys(serverValuesForMainValues) + .filter(key => key.startsWith(attributeCategoryOptionsId)) + .reduce((acc, key) => { + const categoryId = key.split('-')[1]; + acc[categoryId] = serverValuesForMainValues[key]; + return acc; + }, {}); + + const formServerValues = serverValuesForFormValues[Section.groups.ENROLLMENT]; + const currentEventValues = serverValuesForFormValues[Section.groups.EVENT]; + + + const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ + firstStageMetadata: firstStage, + programId, + orgUnitId, + currentEventValues, + fieldsValue: dataEntryFieldValues, + attributeCategoryOptions, + }); + + const autoGenerateEvents = deriveAutoGenerateEvents({ + firstStageMetadata: firstStage, + stages, + enrolledAt, + occurredAt, + programId, + orgUnitId, + attributeCategoryOptions, + }); + + const allEventsToBeCreated = firstStageDuringRegistrationEvent + ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] + : autoGenerateEvents; + + const enrollment = { + program: programId, + status: 'ACTIVE', + orgUnit: orgUnitId, + occurredAt, + enrolledAt, + attributes: deriveAttributesFromFormValues(formServerValues), + events: allEventsToBeCreated, + }; + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + trackedEntity: teiId || generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [enrollment], + }; + }; + + return { + buildTeiWithEnrollment, + }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js index 24b68e9351..4a84d7ccb7 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js @@ -4,7 +4,6 @@ import { useEffect, useRef } from 'react'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { startNewEnrollmentDataEntryInitialisation } from '../EnrollmentRegistrationEntry.actions'; import { scopeTypes, getProgramThrowIfNotFound } from '../../../../metaData'; -import { useLocationQuery } from '../../../../utils/routing'; import { useScopeInfo } from '../../../../hooks/useScopeInfo'; import { useFormValues } from './index'; import type { InputAttribute } from './useFormValues'; @@ -19,8 +18,8 @@ export const useLifecycle = ( trackedEntityInstanceAttributes?: Array, orgUnit: ?OrgUnit, teiId: ?string, + programId: string, ) => { - const { programId } = useLocationQuery(); const dataEntryReadyRef = useRef(false); const dispatch = useDispatch(); const program = programId && getProgramThrowIfNotFound(programId); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js index 6f1efdc57e..4c3b8badab 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js @@ -10,6 +10,7 @@ import { TeiRegistrationEntryComponent } from './TeiRegistrationEntry.component' import { useFormValuesFromSearchTerms } from './hooks/useFormValuesFromSearchTerms'; import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { useBuildTeiPayload } from './hooks/useBuildTeiPayload'; const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) => { const dispatch = useDispatch(); @@ -42,13 +43,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) = }; -export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, orgUnitId, ...rest }) => { +export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, orgUnitId, onSave, ...rest }) => { const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id, orgUnitId); const ready = useSelector(({ dataEntries }) => (!!dataEntries[id])); const dataEntry = useSelector(({ dataEntries }) => (dataEntries[id])); const { registrationMetaData: teiRegistrationMetadata, } = useMetadataForRegistrationForm({ selectedScopeId }); + const { buildTeiWithoutEnrollment } = useBuildTeiPayload({ + trackedEntityTypeId: selectedScopeId, + dataEntryId: id, + orgUnitId, + }); const dataEntryKey = useMemo(() => { if (dataEntry) { @@ -66,6 +72,11 @@ export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, return null; } + const onSaveWithoutEnrollment = () => { + const teiPayload = buildTeiWithoutEnrollment(); + onSave(teiPayload); + }; + return ( = ({ selectedScopeId, ready={ready && !!teiRegistrationMetadata} trackedEntityName={trackedEntityName} isUserInteractionInProgress={isUserInteractionInProgress} + onSave={onSaveWithoutEnrollment} {...rest} /> ); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js index dcc02c58b4..43afd9adc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js @@ -2,8 +2,10 @@ import type { Node } from 'react'; 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 { + TeiPayload, +} from '../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type OwnProps = $ReadOnly<{| id: string, @@ -11,11 +13,11 @@ export type OwnProps = $ReadOnly<{| selectedScopeId: string, saveButtonText: string, fieldOptions?: Object, - onSave: SaveForDuplicateCheck, + onSave: (TeiPayload) => void, duplicatesReviewPageSize: number, isSavingInProgress?: boolean, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}>; @@ -39,9 +41,9 @@ type PropsAddedInHOC = {| |}; type PropsRemovedInHOC = {| renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, duplicatesReviewPageSize: number, - onSave: SaveForDuplicateCheck, + onSave: (TeiPayload) => void, |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js new file mode 100644 index 0000000000..0c4cd6da0d --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js @@ -0,0 +1,81 @@ +// @flow +import { useSelector } from 'react-redux'; +import { useMetadataForRegistrationForm } from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import type { + TeiPayload, +} from '../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; + +type Props = { + trackedEntityTypeId: string, + dataEntryId: string, + orgUnitId: string, + itemId?: string, +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + return formFoundation.convertValues(formValues, convertFormToClient); +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +export const useBuildTeiPayload = ({ + trackedEntityTypeId, + dataEntryId, + itemId = 'newTei', + orgUnitId, +}: Props) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId: trackedEntityTypeId }); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + + const buildTeiWithoutEnrollment = (): TeiPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertValues(clientValues, convertClientToServer); + + // $FlowFixMe + const attributes = Object.keys(serverValuesForFormValues) + .map(key => ({ + attribute: key, + value: serverValuesForFormValues[key], + })); + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + attributes, + trackedEntity: generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [], + }; + }; + + return { buildTeiWithoutEnrollment }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js index a727db9ec2..f55dc39c0e 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js @@ -1,7 +1,7 @@ // @flow -import { ProgramStage, RenderFoundation } from '../../../../../../metaData'; +import type { EnrollmentPayload } from '../../../../EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { TeiPayload } from '../../../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type SaveForDuplicateCheck = ( - formFoundation?: RenderFoundation, - firstStageMetaData?: { stage: ProgramStage }, + teiWithEnrollment: EnrollmentPayload | TeiPayload, ) => void; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js index 98525d23d5..d74476bfee 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js @@ -1,9 +1,8 @@ // @flow import { type InputSearchGroup } from '../../../../../metaData'; -import type { SaveForDuplicateCheck } from './types'; export type Input = {| - onSave: SaveForDuplicateCheck, + onSave: () => void, hasDuplicate: ?boolean, onResetPossibleDuplicates: () => void, onReviewDuplicates: (duplicatesReviewPageSize: number) => void, diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js index 87a3d4cffc..560dd0e9a2 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js @@ -2,16 +2,15 @@ import type { Node } from 'react'; import type { Enrollment, TeiRegistration } from '../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../CardList'; -import type { SaveForDuplicateCheck } from './types'; export type Props = { id: string, selectedScopeId: string, - onSave: SaveForDuplicateCheck, + onSave: () => void, enrollmentMetadata?: Enrollment, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: () => void) => Node, skipDuplicateCheck: ?boolean, }; diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js index e716f71741..6beb555bc6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js @@ -1,7 +1,12 @@ // @flow -import type { ProgramStage, RenderFoundation } from '../../../../metaData'; import { actionCreator } from '../../../../actions/actions.utils'; import { effectMethods } from '../../../../trackerOffline'; +import type { + EnrollmentPayload, +} from '../../../DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { + TeiPayload, +} from '../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export const registrationFormActionTypes = { NEW_TRACKED_ENTITY_INSTANCE_SAVE_START: 'StartSavingNewTrackedEntityInstance', @@ -16,8 +21,8 @@ export const registrationFormActionTypes = { }; // without enrollment -export const startSavingNewTrackedEntityInstance = (formFoundation: RenderFoundation) => - actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ formFoundation }); +export const startSavingNewTrackedEntityInstance = (teiPayload: TeiPayload) => + actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ teiPayload }); export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE)( @@ -41,11 +46,9 @@ export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => ); // with enrollment -export const startSavingNewTrackedEntityInstanceWithEnrollment = (formFoundation: RenderFoundation, teiId: string, uid: string, firstStage?: ProgramStage) => +export const startSavingNewTrackedEntityInstanceWithEnrollment = (enrollmentPayload: EnrollmentPayload, uid: string) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START)({ - formFoundation, - teiId, - firstStage, + enrollmentPayload, uid, }); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js index 599414df09..d5d956f29d 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js @@ -15,9 +15,6 @@ import { ResultsPageSizeContext } from '../../shared-contexts'; import { navigateToEnrollmentOverview } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { useLocationQuery } from '../../../../utils/routing'; import { EnrollmentRegistrationEntryWrapper } from '../EnrollmentRegistrationEntryWrapper.component'; -import { - useMetadataForRegistrationForm, -} from '../../../DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm'; import { useCurrentOrgUnitInfo } from '../../../../hooks/useCurrentOrgUnitInfo'; const getStyles = ({ typography }) => ({ @@ -100,7 +97,6 @@ const RegistrationDataEntryPlain = ({ const { resultsPageSize } = useContext(ResultsPageSizeContext); const { scopeType, programName, trackedEntityName } = useScopeInfo(selectedScopeId); const titleText = useScopeTitleText(selectedScopeId); - const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const { id: reduxOrgUnitId } = useCurrentOrgUnitInfo(); const handleRegistrationScopeSelection = (id) => { @@ -183,9 +179,7 @@ const RegistrationDataEntryPlain = ({ orgUnitId={reduxOrgUnitId} teiId={teiId} selectedScopeId={selectedScopeId} - onSave={(customFormFoundation, firstStageMetaData) => - onSaveWithEnrollment(customFormFoundation, firstStageMetaData?.stage) - } + onSave={onSaveWithEnrollment} saveButtonText={(trackedEntityTypeNameLC: string) => i18n.t('Save {{trackedEntityTypeName}}', { trackedEntityTypeName: trackedEntityTypeNameLC, interpolation: { escapeValue: false }, @@ -242,7 +236,7 @@ const RegistrationDataEntryPlain = ({ trackedEntityName, interpolation: { escapeValue: false }, })} - onSave={() => onSaveWithoutEnrollment(formFoundation)} + onSave={onSaveWithoutEnrollment} duplicatesReviewPageSize={resultsPageSize} renderDuplicatesDialogActions={renderDuplicatesDialogActions} renderDuplicatesCardActions={renderDuplicatesCardActions} diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js index 41f5a864d5..8aeda04f1f 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js @@ -26,15 +26,15 @@ export const RegistrationDataEntry: ComponentType = ({ const { teiId } = useLocationQuery(); const dispatchOnSaveWithoutEnrollment = useCallback( - (formFoundation) => { dispatch(startSavingNewTrackedEntityInstance(formFoundation)); }, + (teiPayload) => { dispatch(startSavingNewTrackedEntityInstance(teiPayload)); }, [dispatch]); const dispatchOnSaveWithEnrollment = useCallback( - (formFoundation, firstStage) => { + (enrollmentPayload) => { const uid = uuid(); - dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(formFoundation, teiId, uid, firstStage)); + dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(enrollmentPayload, uid)); }, - [dispatch, teiId]); + [dispatch]); const dataEntryIsReady = useSelector(({ dataEntries }) => (!!dataEntries[dataEntryId])); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js index 004adb75f7..be02acbcb5 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js @@ -1,59 +1,31 @@ // @flow import { ofType } from 'redux-observable'; -import { pipe } from 'capture-core-utils'; import { flatMap, map } from 'rxjs/operators'; import { of, EMPTY } from 'rxjs'; -import { FEATURETYPE, dataEntryKeys } from 'capture-core/constants'; +import { dataEntryKeys } from 'capture-core/constants'; import { registrationFormActionTypes, saveNewTrackedEntityInstance, saveNewTrackedEntityInstanceWithEnrollment, } from './RegistrationDataEntry.actions'; -import { getTrackerProgramThrowIfNotFound, dataElementTypes, Section } from '../../../../metaData'; +import { getTrackerProgramThrowIfNotFound } from '../../../../metaData'; import { navigateToEnrollmentOverview, } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; -import { convertFormToClient, convertClientToServer } from '../../../../converters'; import { buildUrlQueryString, shouldUseNewDashboard } from '../../../../utils/routing'; import { - deriveAutoGenerateEvents, - deriveFirstStageDuringRegistrationEvent, getStageWithOpenAfterEnrollment, - standardGeoJson, PAGES, } from './helpers'; -const convertFn = pipe(convertFormToClient, convertClientToServer); - -const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); - -const deriveAttributesFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => !geometryType(key)) - .map(key => ({ attribute: key, value: formValues[key] })); - -const deriveGeometryFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => geometryType(key)) - .reduce((acc, currentKey) => (standardGeoJson(formValues[currentKey])), undefined); - -export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable, store: ReduxStore) => +export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable) => action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START), map((action) => { - const { currentSelections: { orgUnitId, trackedEntityTypeId }, formsValues } = store.value; - const values = formsValues['newPageDataEntryId-newTei']; - const formFoundation = action.payload?.formFoundation; - const formServerValues = formFoundation?.convertValues(values, convertFn); + const { teiPayload } = action.payload; return saveNewTrackedEntityInstance( { - trackedEntities: [{ - attributes: deriveAttributesFromFormValues(formServerValues), - geometry: deriveGeometryFromFormValues(values), - enrollments: [], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityTypeId, - }], + trackedEntities: [teiPayload], }); }), ); @@ -80,78 +52,26 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START), map((action) => { - const formId = 'newPageDataEntryId-newEnrollment'; - const { currentSelections: { orgUnitId, programId }, formsValues, dataEntriesFieldsValue } = store.value; + const { currentSelections: { programId } } = store.value; const { dataStore, userDataStore, temp } = store.value.useNewDashboard; - const { formFoundation, teiId: trackedEntity, firstStage: firstStageMetadata, uid } = action.payload; - const fieldsValue = dataEntriesFieldsValue[formId] || {}; - const { occurredAt, enrolledAt, geometry } = fieldsValue; - const attributeCategoryOptionsId = 'attributeCategoryOptions'; - const attributeCategoryOptions = Object.keys(fieldsValue) - .filter(key => key.startsWith(attributeCategoryOptionsId)) - .reduce((acc, key) => { - const categoryId = key.split('-')[1]; - acc[categoryId] = fieldsValue[key]; - return acc; - }, {}); - const { trackedEntityType, stages } = getTrackerProgramThrowIfNotFound(programId); - const currentFormData = formsValues[formId] || {}; + const { enrollmentPayload, uid } = action.payload; + const { stages, useFirstStageDuringRegistration } = getTrackerProgramThrowIfNotFound(programId); + const shouldRedirect = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); const { stageWithOpenAfterEnrollment, redirectTo } = getStageWithOpenAfterEnrollment( stages, - firstStageMetadata, + useFirstStageDuringRegistration, shouldRedirect, ); - const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFn); - const formServerValues = convertedValues[Section.groups.ENROLLMENT]; - const currentEventValues = convertedValues[Section.groups.EVENT]; - - const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ - firstStageMetadata, - programId, - orgUnitId, - currentEventValues, - fieldsValue, - attributeCategoryOptions, - }); - const autoGenerateEvents = deriveAutoGenerateEvents({ - stages, - enrolledAt, - occurredAt, - programId, - orgUnitId, - firstStageMetadata, - attributeCategoryOptions, - }); - const allEventsToBeCreated = firstStageDuringRegistrationEvent - ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] - : autoGenerateEvents; - const eventIndex = allEventsToBeCreated.findIndex( + const eventIndex = enrollmentPayload.enrollments[0]?.events.findIndex( eventsToBeCreated => eventsToBeCreated.programStage === stageWithOpenAfterEnrollment?.id, ); return saveNewTrackedEntityInstanceWithEnrollment({ candidateForRegistration: { trackedEntities: [ - { - geometry: deriveGeometryFromFormValues(currentFormData), - enrollments: [ - { - geometry: standardGeoJson(geometry), - occurredAt: convertFn(occurredAt, dataElementTypes.DATE), - enrolledAt: convertFn(enrolledAt, dataElementTypes.DATE), - program: programId, - orgUnit: orgUnitId, - attributes: deriveAttributesFromFormValues(formServerValues), - status: 'ACTIVE', - events: allEventsToBeCreated, - }, - ], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityType.id, - ...(trackedEntity && { trackedEntity }), - }, + enrollmentPayload, ], }, redirectTo, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js index a38c30e4e5..e36a6e10e6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js @@ -24,7 +24,7 @@ export const deriveAutoGenerateEvents = ({ occurredAt: string, programId: string, orgUnitId: string, - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, attributeCategoryOptions: { [categoryId: string]: string } | string, }) => { // in case we have a program that does not have an incident date (occurredAt), such as Malaria case diagnosis, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js index bf40f45135..2e4f952f92 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js @@ -16,7 +16,7 @@ export const deriveFirstStageDuringRegistrationEvent = ({ fieldsValue, attributeCategoryOptions, }: { - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, programId: string, orgUnitId: string, currentEventValues?: { [id: string]: any }, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js index 26bd03daad..23d9d1a610 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js @@ -12,7 +12,7 @@ export const PAGES = { // when the event will not be created redirect to enrollmentEventNew export const getStageWithOpenAfterEnrollment = ( stages: Map, - firstStageMetadata: ProgramStage, + useFirstStageDuringRegistration: boolean, shouldRedirect: boolean, ) => { const stagesArray = [...stages.values()]; @@ -22,8 +22,8 @@ export const getStageWithOpenAfterEnrollment = ( if (shouldRedirect && firstStageWithOpenAfterEnrollment) { // event will be created during first stage registration if ( - firstStageMetadata && - firstStageMetadata.id === firstStageWithOpenAfterEnrollment.id + useFirstStageDuringRegistration + && stagesArray[0].id === firstStageWithOpenAfterEnrollment.id ) { return PAGES.enrollmentEventEdit; } diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js index a3ac0f0ae5..20f5463e75 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -3,16 +3,18 @@ import type { Node } from 'react'; import type { TeiRegistration } from '../../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../../CardList'; import type { - SaveForEnrollmentAndTeiRegistration, ExistingUniqueValueDialogActionsComponent, } from '../../../../../DataEntries'; +import type { + TeiPayload, +} from '../../../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type Props = {| theme: Theme, - onSave: SaveForEnrollmentAndTeiRegistration, + onSave: (TeiPayload) => void, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js index 4b0ad71c40..292a1d1530 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -6,11 +6,13 @@ import { DataEntryTrackedEntityInstance } from './TrackedEntityInstance'; type Props = { showDataEntry: boolean, programId: string, + onSaveWithoutEnrollment: () => void, + onSaveWithEnrollment: () => void, }; export class RegisterTeiDataEntryComponent extends React.Component { render() { - const { showDataEntry, programId, ...passOnProps } = this.props; + const { showDataEntry, programId, onSaveWithoutEnrollment, onSaveWithEnrollment, ...passOnProps } = this.props; if (!showDataEntry) { return null; @@ -20,6 +22,7 @@ export class RegisterTeiDataEntryComponent extends React.Component { return ( ); } @@ -27,6 +30,7 @@ export class RegisterTeiDataEntryComponent extends React.Component { return ( ); } diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js index fb36df54e1..b4f9617052 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -3,17 +3,28 @@ import type { Node } from 'react'; import type { TeiRegistration } from '../../../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../../../CardList'; import type { - SaveForEnrollmentAndTeiRegistration, ExistingUniqueValueDialogActionsComponent, } from '../../../../../../DataEntries'; +export type TeiPayload = {| + trackedEntity: string, + trackedEntityType: string, + enrollments: [], + orgUnit: string, + geometry: ?{ coordinates: any, type: any }, + attributes: Array<{| + attribute: string, + value: any, + |}>, +|} + export type Props = {| theme: Theme, trackedEntityTypeId: string, - onSave: SaveForEnrollmentAndTeiRegistration, + onSave: TeiPayload => void, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js index 4bee8f13be..9a9a7070af 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -58,7 +58,8 @@ const DialogButtons = ({ onCancel, onSave, trackedEntityName }) => ( const RegisterTeiPlain = ({ dataEntryId, onLink, - onSave, + onSaveWithoutEnrollment, + onSaveWithEnrollment, onGetUnsavedAttributeValues, trackedEntityName, trackedEntityTypeId, @@ -101,7 +102,8 @@ const RegisterTeiPlain = ({ /> { const dataEntryId = 'relationship'; - const itemId = useSelector(({ dataEntries }) => dataEntries[dataEntryId]?.itemId); const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); const selectedScopeId = suggestedProgramId || trackedEntityTypeId; const { trackedEntityName } = useScopeInfo(selectedScopeId); - const { buildTeiPayload } = useDataEntryReduxConverter({ - dataEntryId, - itemId, - trackedEntityTypeId, - }); - - const onCreateNewTei = () => { - const teiPayload = buildTeiPayload(); - onSave(teiPayload); - }; return ( void, + onSaveWithEnrollment: () => void, + onSaveWithoutEnrollment: () => void, ...SharedProps, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js deleted file mode 100644 index 9ec22b3a0b..0000000000 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js +++ /dev/null @@ -1,148 +0,0 @@ -// @flow -import { useSelector } from 'react-redux'; -import moment from 'moment'; -import { getDataEntryKey } from '../../../../../DataEntry/common/getDataEntryKey'; -import { getTrackedEntityTypeThrowIfNotFound, getTrackerProgramThrowIfNotFound } from '../../../../../../metaData'; -import type { RenderFoundation } from '../../../../../../metaData'; -import { convertClientToServer, convertFormToClient } from '../../../../../../converters'; -import { - convertDataEntryValuesToClientValues, -} from '../../../../../DataEntry/common/convertDataEntryValuesToClientValues'; -import { getFormattedStringFromMomentUsingEuropeanGlyphs } from '../../../../../../../capture-core-utils/date'; -import { capitalizeFirstLetter } from '../../../../../../../capture-core-utils/string'; -import { generateUID } from '../../../../../../utils/uid/generateUID'; - -type DataEntryReduxConverterProps = { - dataEntryId: string; - itemId: string; - trackedEntityTypeId: string; -}; - -function getMetadata(programId: ?string, tetId: string) { - return programId ? getTrackerProgramMetadata(programId) : getTETMetadata(tetId); -} - -function getTrackerProgramMetadata(programId: string) { - const program = getTrackerProgramThrowIfNotFound(programId); - return { - form: program.enrollment.enrollmentForm, - attributes: program.trackedEntityType.attributes, - tetName: program.trackedEntityType.name, - }; -} - -function getTETMetadata(tetId: string) { - const tet = getTrackedEntityTypeThrowIfNotFound(tetId); - return { - form: tet.teiRegistration.form, - attributes: tet.attributes, - tetName: tet.name, - }; -} - -function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { - const clientValues = formFoundation.convertValues(formValues, convertFormToClient); - return clientValues; -} - -function getServerValuesForMainValues( - values: Object, - meta: Object, - formFoundation: RenderFoundation, -) { - const clientValues = convertDataEntryValuesToClientValues( - values, - meta, - formFoundation, - ) || {}; - - // potientally run this through a server to client converter for enrollment, the same way as for event - const serverValues = Object - .keys(clientValues) - .reduce((acc, key) => { - const value = clientValues[key]; - const type = meta[key].type; - acc[key] = convertClientToServer(value, type); - return acc; - }, {}); - - return serverValues; -} - -function getPossibleTetFeatureTypeKey(serverValues: Object) { - return Object - .keys(serverValues) - .find(key => key.startsWith('FEATURETYPE_')); -} - -function buildGeometryProp(key: string, serverValues: Object) { - if (!serverValues[key]) { - return undefined; - } - const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); - return { - type, - coordinates: serverValues[key], - }; -} - -export const useDataEntryReduxConverter = ({ - dataEntryId, - itemId, - trackedEntityTypeId, -}: DataEntryReduxConverterProps) => { - const dataEntryKey = getDataEntryKey(dataEntryId, itemId); - const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); - const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); - const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); - const { programId, orgUnit } = useSelector(({ newRelationshipRegisterTei }) => newRelationshipRegisterTei); - - const buildTeiPayload = () => { - const { form: formFoundation } = getMetadata(programId, trackedEntityTypeId); - const clientValues = getClientValuesForFormData(formValues, formFoundation); - const serverValuesForFormValues = formFoundation.convertValues(clientValues, convertClientToServer); - const serverValuesForMainValues = getServerValuesForMainValues( - dataEntryFieldValues, - dataEntryFieldsMeta, - formFoundation, - ); - - // $FlowFixMe - const attributes = Object.keys(serverValuesForFormValues) - .map(key => ({ - attribute: key, - value: serverValuesForFormValues[key], - })); - - const enrollment = programId ? { - program: programId, - status: 'ACTIVE', - orgUnit: orgUnit.id, - occurredAt: getFormattedStringFromMomentUsingEuropeanGlyphs(moment()), - attributes, - ...serverValuesForMainValues, - } : null; - - const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); - let geometry; - if (tetFeatureTypeKey) { - geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); - delete serverValuesForFormValues[tetFeatureTypeKey]; - } - - return { - // $FlowFixMe - attributes: !enrollment ? attributes : undefined, - trackedEntity: generateUID(), - orgUnit: orgUnit.id, - trackedEntityType: trackedEntityTypeId, - geometry, - enrollments: enrollment ? [enrollment] : [], - }; - }; - - - return { - buildTeiPayload, - }; -}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js deleted file mode 100644 index a885a48a6d..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.component.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { NoticeBox } from '@dhis2/ui'; -import type { Props } from './ErrorText.types'; - -export const ErrorText = ({ stageName }: Props) => ( - <> -
- - - {i18n.t("You can't add any more {{ programStageName }} events", { - programStageName: stageName, - interpolation: { escapeValue: false }, - })} - - -
- -); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js deleted file mode 100644 index 07785c7e1a..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/ErrorText.types.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow - -export type Props = {| - stageName: string, -|}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js deleted file mode 100644 index 2b26086840..0000000000 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/ErrorText/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { ErrorText } from './ErrorText.component'; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js index 1c0a36c1c8..465f807ace 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/FinishButtons.component.js @@ -3,7 +3,6 @@ import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles } from '@material-ui/core'; import { Button, spacersNum } from '@dhis2/ui'; -import { ConditionalTooltip } from 'capture-core/components/ConditionalTooltip'; import { withCancelButton } from '../../DataEntry/withCancelButton'; import { addEventSaveTypes } from '../DataEntry/addEventSaveTypes'; import type { InputProps, Props } from './finishButtons.types'; @@ -18,40 +17,17 @@ const styles = { }, }; -const FinishButtonsPlain = ({ onSave, cancelButton, hiddenProgramStage, stageName, classes }: Props) => ( +const FinishButtonsPlain = ({ onSave, cancelButton, classes }: Props) => (
- - - +
- - - +
{cancelButton}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js index 079708e30a..dd3acb8e32 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/FinishButtons/finishButtons.types.js @@ -6,14 +6,10 @@ export type InputProps = {| onSave: (saveType: $Keys) => void, onCancel: () => void, id: string, - hiddenProgramStage: boolean, - stageName: string, |}; export type Props = {| onSave: (saveType: $Keys) => void, cancelButton: Element, - hiddenProgramStage: boolean, - stageName: string, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.component.js index 4f784d2b9a..4e3f2f96ef 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,6 @@ import { Widget } from '../../Widget'; import { DataEntry } from '../DataEntry'; import { FinishButtons } from '../FinishButtons'; import { SavingText } from '../SavingText'; -import { ErrorText } from '../ErrorText'; import type { Props } from './validated.types'; const styles = () => ({ @@ -24,7 +23,6 @@ const ValidatedPlain = ({ onSave, onCancel, orgUnit, - hiddenProgramStage, id, ...passOnProps }: Props) => ( @@ -48,14 +46,8 @@ const ValidatedPlain = ({ onSave={onSave} onCancel={onCancel} id={id} - hiddenProgramStage={hiddenProgramStage} - stageName={stage.name} /> - {hiddenProgramStage ? ( - - ) : ( - - )} + )} diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js index c0245b477f..3d85e67cbf 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/Validated.container.js @@ -1,6 +1,6 @@ // @flow import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { withAskToCreateNew, withSaveHandler } from '../../DataEntry'; import { useLifecycle } from './useLifecycle'; import { useClientFormattedRulesExecutionDependencies } from './useClientFormattedRulesExecutionDependencies'; @@ -29,11 +29,6 @@ export const Validated = ({ }: ContainerProps) => { const dataEntryId = 'enrollmentEvent'; const itemId = 'newEvent'; - const rulesEffectsHiddenProgram = useSelector( - ({ rulesEffectsHiddenProgramStageDesc }) => - rulesEffectsHiddenProgramStageDesc && rulesEffectsHiddenProgramStageDesc[`${dataEntryId}-${itemId}`], - ); - const hiddenProgramStage = rulesEffectsHiddenProgram && rulesEffectsHiddenProgram[stage.id]; const rulesExecutionDependenciesClientFormatted = useClientFormattedRulesExecutionDependencies(rulesExecutionDependencies, program); @@ -132,7 +127,6 @@ export const Validated = ({ programName={program.name} orgUnit={orgUnit} rulesExecutionDependenciesClientFormatted={rulesExecutionDependenciesClientFormatted} - hiddenProgramStage={hiddenProgramStage} /> ); }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js index 932e61a6d8..a5194d88af 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/validated.types.js @@ -25,6 +25,5 @@ export type Props = {| formRef: (formInstance: any) => void, dataEntryFieldRef: (instance: any, id: string) => void, rulesExecutionDependenciesClientFormatted: RulesExecutionDependenciesClientFormatted, - hiddenProgramStage: boolean, ...CssClasses, |}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index a97e33314f..66eae92b47 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -127,7 +127,7 @@ const NewTrackedEntityRelationshipPlain = ({ }, }; - addRelationship({ + const payload = { apiData: { trackedEntities: [trackedEntity], relationships: [{ @@ -137,7 +137,9 @@ const NewTrackedEntityRelationshipPlain = ({ }], }, clientRelationship: clientData, - }); + }; + + addRelationship(payload); }, [addRelationship, selectedLinkedEntityMetadata, teiId]); const handleNavigation = useCallback( diff --git a/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js b/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js index 85ab6638ee..30dd29a15f 100644 --- a/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js +++ b/src/core_modules/capture-core/reducers/descriptions/form.reducerDescription.js @@ -230,7 +230,7 @@ export const formsSectionsFieldsUIDesc = createReducerDescription({ return { ...state, - formId: { + [formId]: { ...state[formId], ...updatedFields, },