diff --git a/CHANGELOG.md b/CHANGELOG.md index f5540c833b..e1e2b5e83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [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) + + +### Features + +* [DHIS2-9661][DHIS2-14830] first stage on registration page ([#3267](https://github.com/dhis2/capture-app/issues/3267)) ([6e5f6fe](https://github.com/dhis2/capture-app/commit/6e5f6fea7b6ab043800e806c7459210f10ef608a)) + +## [100.40.1](https://github.com/dhis2/capture-app/compare/v100.40.0...v100.40.1) (2023-09-30) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([b58bc66](https://github.com/dhis2/capture-app/commit/b58bc66db4136ad558c02f38ac54a2319b3af3f1)) + # [100.40.0](https://github.com/dhis2/capture-app/compare/v100.39.4...v100.40.0) (2023-09-20) diff --git a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js b/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js index 45ad92f517..70a2fdad96 100644 --- a/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js +++ b/cypress/integration/EnrollmentPage/BreakingTheGlass/index.js @@ -31,6 +31,10 @@ And('you create a new tei in Child programme from Ngelehun CHC', () => { .eq(1) .type('TheGlass') .blur(); + cy.get('[data-test="capture-ui-input"]') + .eq(7) + .type('2023-09-01') + .blur(); clickSave(); }); diff --git a/cypress/integration/NewPage.feature b/cypress/integration/NewPage.feature index 21b3d996d8..85aaa709d3 100644 --- a/cypress/integration/NewPage.feature +++ b/cypress/integration/NewPage.feature @@ -178,6 +178,7 @@ Feature: User creates a new entries from the registration page Scenario: New person in Tracker Program > Submitting the form shows a list with duplicates Given you are in Child programme registration page When you fill the Child programme registration form with a first name with value that has duplicates + And you fill in the birth report date And you click the save person submit button And you see the possible duplicates modal When you click the next page button @@ -197,7 +198,7 @@ Feature: User creates a new entries from the registration page When you are in the Malaria case diagnosis, treatment and investigation program registration page And you fill the Malaria case diagnosis registration form with values And you click the save malaria entity submit button - Then you see the enrollment event New page + Then you see the enrollment event Edit page When you open the main page with Ngelehun and Malaria case diagnosis, treatment and investigation context And you opt out to use the new enrollment Dashboard for Malaria case diagnosis, treatment and investigation Then you see the opt in component for Malaria case diagnosis, treatment and investigation @@ -208,3 +209,7 @@ Feature: User creates a new entries from the registration page Given you are in Child programme reenrollment page Then you see the form prefield with existing TEI attributes values And the scope selector has the TEI context + + Scenario: First stage appears on registration page + Given you are in Child programme registration page + Then the first stage appears on registration page diff --git a/cypress/integration/NewPage/index.js b/cypress/integration/NewPage/index.js index 09046c7c65..4cc6af0afb 100644 --- a/cypress/integration/NewPage/index.js +++ b/cypress/integration/NewPage/index.js @@ -502,6 +502,13 @@ And('you fill the Child programme registration form with a first name with value .blur(); }); +And('you fill in the birth report date', () => { + cy.get('[data-test="capture-ui-input"]') + .eq(7) + .type('2023-01-01') + .blur(); +}); + And('you are in the WNCH PNC program registration page', () => { cy.visit('/#/new?programId=uy2gU8kT1jF&orgUnitId=DiszpKrYNg8'); }); @@ -561,12 +568,10 @@ And('you fill the Malaria case diagnosis registration form with values', () => { .blur(); }); -Then('you see the enrollment event New page', () => { - cy.url().should('include', '/#/enrollmentEventNew?'); - cy.url().should('include', 'stageId=hYyB7FUS5eR'); +Then('you see the enrollment event Edit page', () => { + cy.url().should('include', '/#/enrollmentEventEdit?'); }); - And('you fill in multiple Allergies options', () => { cy.get('[data-test="registration-page-content"]').within(() => { cy.contains('Allergies').should('exist'); @@ -585,3 +590,13 @@ Then('you can see the multiple selections in the form', () => { cy.contains('Other').should('exist'); }); }); + +Then('the first stage appears on registration page', () => { + cy.get('[data-test="registration-page-content"]').within(() => { + cy.contains('Birth - Basic info').should('exist'); + cy.contains('Birth - Details').should('exist'); + cy.contains('Birth - Status').should('exist'); + cy.contains('Report date').should('exist'); + cy.contains('Apgar Score').should('exist'); + }); +}); diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature index fe69bbc858..d3f07adfe2 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature +++ b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser.feature @@ -233,7 +233,7 @@ And the Custom Program stage list is deleted @v>=40 Scenario: The user can save a program stage working list, based on a TEI working list configuration -Given you open the main page with Ngelehun and Malaria focus investigation context +Given you open a clean main page with Ngelehun and Malaria focus investigation context Then you see the custom TEI working lists And you can load the view with the name Ongoing foci responses And you open the program stage filters from the more filters dropdown menu diff --git a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js index dedb6cd458..7e5825c8a1 100644 --- a/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js +++ b/cypress/integration/WorkingLists/TeiWorkingLists/TeiWorkingListsUser/index.js @@ -2,6 +2,19 @@ import { v4 as uuid } from 'uuid'; import '../../sharedSteps'; import '../../../sharedSteps'; +const cleanUpIfApplicable = (programId) => { + cy.buildApiUrl(`programStageWorkingLists?filter=program.id:eq:${programId}&fields=id,displayName`) + .then(url => cy.request(url)) + .then(({ body }) => { + const workingList = body.programStageWorkingLists?.find(e => e.displayName === 'Custom Program stage list'); + if (!workingList) { + return null; + } + return cy + .buildApiUrl('programStageWorkingLists', workingList.id) + .then(workingListUrl => cy.request('DELETE', workingListUrl)); + }); +}; Given('you open the main page with Ngelehun and child programme context', () => { cy.visit('#/?programId=IpHINAT79UW&orgUnitId=DiszpKrYNg8'); }); @@ -42,6 +55,7 @@ Given('you open the main page with Ngelehun and Malaria case diagnosis context', }); Given('you open the main page with Ngelehun and Malaria case diagnosis and Household investigation context', () => { + cleanUpIfApplicable('qDkgAbB5Jlk'); cy.visit('#/?programId=qDkgAbB5Jlk&orgUnitId=DiszpKrYNg8'); cy.get('[data-test="tei-working-lists"]') @@ -60,6 +74,11 @@ Given('you open the main page with Ngelehun and Malaria case diagnosis and House .click(); }); +Given('you open a clean main page with Ngelehun and Malaria focus investigation context', () => { + cleanUpIfApplicable('M3xtLkYBlKI'); + cy.visit('#/?programId=M3xtLkYBlKI&orgUnitId=DiszpKrYNg8'); +}); + Then('the default working list should be displayed', () => { const names = [ 'Filona', @@ -740,3 +759,4 @@ Then('the program stage custom working list filters are loaded', () => { .find('[data-test="more-filters"]') .should('have.length', 2); }); + 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/i18n/en.pot b/i18n/en.pot index bc3b72d48c..06e8de8d33 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: 2023-09-25T08:33:17.277Z\n" -"PO-Revision-Date: 2023-09-25T08:33:17.277Z\n" +"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" +"PO-Revision-Date: 2023-09-29T14:14:34.330Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -149,6 +149,15 @@ msgstr "Coordinate" msgid "Enrollment" msgstr "Enrollment" +msgid "Complete event" +msgstr "Complete event" + +msgid "{{ stageName }} - Basic info" +msgstr "{{ stageName }} - Basic info" + +msgid "{{ stageName }} - Status" +msgstr "{{ stageName }} - Status" + msgid "Please select {{categoryName}}" msgstr "Please select {{categoryName}}" @@ -167,15 +176,18 @@ msgstr "Cancel" msgid "Metadata error. see log for details" msgstr "Metadata error. see log for details" +msgid "{{ stageName }} - Details" +msgstr "{{ stageName }} - Details" + +msgid "{{ stageName }} - {{ sectionName }}" +msgstr "{{ stageName }} - {{ sectionName }}" + msgid "Assigned user" msgstr "Assigned user" msgid "Search for user" msgstr "Search for user" -msgid "Complete event" -msgstr "Complete event" - msgid "Basic info" msgstr "Basic info" diff --git a/i18n/zh.po b/i18n/zh.po index 77abc5059a..3776db9b73 100644 --- a/i18n/zh.po +++ b/i18n/zh.po @@ -320,7 +320,7 @@ msgid "" msgstr "一个具有 {{attributeName}} 的 {{trackedEntityTypeName}} 实体已经登记" msgid "Show registered {{trackedEntityTypeName}}" -msgstr "显示登记的 {{attributeName}} " +msgstr "显示已注册的{{trackedEntityTypeName}}" msgid "Registered person" msgstr "登记的人" @@ -1069,7 +1069,7 @@ msgid "To work with the selected program," msgstr "基于选择的项目工作" msgid "open the Tracker Capture app" -msgstr "打开跟踪随访采集APP" +msgstr "打开跟踪记录APP" msgid "This program is protected" msgstr "该项目受保护" @@ -1257,7 +1257,7 @@ msgid "Save changes" msgstr "保存修改" msgid "Profile" -msgstr "简历" +msgstr "基本信息" msgid "There is a problem with this form" msgstr "这个表单有问题" @@ -1278,7 +1278,7 @@ msgid "Profile widget could not be loaded. Please try again later" msgstr "无法加载个人资料窗口小部件。请稍后再试" msgid "{{TETName}} profile" -msgstr "{{TETName}} 基本资料" +msgstr "{{TETName}} 基本信息" msgid "Edit" msgstr "编辑" @@ -1314,7 +1314,7 @@ msgid "{{ overdueEvents }} overdue" msgstr "{{ overdueEvents }} 逾期" msgid "{{ scheduledEvents }} scheduled" -msgstr "{{scheduledEvents}} 已调度" +msgstr "{{ ScheduledEvents }} 已安排" msgid "Stages and Events" msgstr "阶段与活动" @@ -1416,7 +1416,7 @@ msgid "Scheduled" msgstr "已经调度" msgid "Overdue{{ escape }} due {{ time }}" -msgstr "逾期{{逃脱}}到期{{时间}}" +msgstr "逾期{{ escape }}到期{{ time }}" msgid "Overdue" msgstr "逾期" diff --git a/package.json b/package.json index 9acdd9c1cb..348ef10bf7 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "capture-app", "homepage": ".", - "version": "100.40.0", - "cacheVersion": "5", + "version": "100.41.1", + "cacheVersion": "6", "serverVersion": "38", "license": "BSD-3-Clause", "private": true, @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.40.0", + "@dhis2/rules-engine-javascript": "100.41.1", "@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 c3b109cd88..5aa6daf77f 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.40.0", + "version": "100.41.1", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/src/core_modules/capture-core/components/D2Form/D2Form.component.js b/src/core_modules/capture-core/components/D2Form/D2Form.component.js index 5d24c11bba..4f17677b07 100644 --- a/src/core_modules/capture-core/components/D2Form/D2Form.component.js +++ b/src/core_modules/capture-core/components/D2Form/D2Form.component.js @@ -116,11 +116,21 @@ class D2Form extends React.PureComponent { id, classes, isFormInReduxStore, + getCustomContent, ...passOnProps } = this.props; const metaDataSectionsAsArray = Array.from(formFoundation.sections.entries()).map(entry => entry[1]); - const sections = metaDataSectionsAsArray.map(section => (passOnProps.formHorizontal ? this.renderHorizontal(section, passOnProps) : this.renderVertical(section, passOnProps))); + const sections = metaDataSectionsAsArray.map(section => ( + passOnProps.formHorizontal + ? this.renderHorizontal(section, passOnProps) + : ( + <> + {getCustomContent && getCustomContent(section.id) } + {this.renderVertical(section, passOnProps)} + + ) + )); return ( <> diff --git a/src/core_modules/capture-core/components/D2Form/D2Form.types.js b/src/core_modules/capture-core/components/D2Form/D2Form.types.js index 59f5065222..9671d8309f 100644 --- a/src/core_modules/capture-core/components/D2Form/D2Form.types.js +++ b/src/core_modules/capture-core/components/D2Form/D2Form.types.js @@ -1,5 +1,5 @@ // @flow - +import * as React from 'react'; import type { RenderFoundation } from '../../metaData'; export type FormRef = $ReadOnly<{| @@ -10,6 +10,7 @@ export type OwnProps = $ReadOnly<{| formFoundation: RenderFoundation, id: string, formHorizontal?: boolean, + getCustomContent?: (beforeSectionId: string) => React.Node, ...FormRef |}> diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js index aa756a6f5a..afb0561e04 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.js @@ -33,7 +33,8 @@ import { getIncidentDateValidatorContainer, } from './fieldValidators'; import { sectionKeysForEnrollmentDataEntry } from './constants/sectionKeys.const'; -import { type Enrollment, getProgramThrowIfNotFound } from '../../../metaData'; +import { type Enrollment, ProgramStage, RenderFoundation, getProgramThrowIfNotFound } from '../../../metaData'; +import { EnrollmentWithFirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry'; import { getCategoryOptionsValidatorContainers, attributeOptionsKey, @@ -41,7 +42,6 @@ import { withAOCFieldBuilder, withDataEntryFields, } from '../../DataEntryDhis2Helpers'; -import { shouldUseNewDashboard } from '../../../utils/routing'; const overrideMessagePropNames = { errorMessage: 'validationError', @@ -290,40 +290,35 @@ const getCategoryOptionsSettingsFn = () => { getPropName: (props: Object, fieldId?: string) => (fieldId ? `${attributeOptionsKey}-${fieldId}` : attributeOptionsKey), getFieldIds: (props: Object) => props.categories?.map(category => category.id), getValidatorContainers: (props: Object, fieldId?: string) => getCategoryOptionsValidatorContainers(props, fieldId), - getMeta: (props: Object) => ({ - section: AOCsectionKey, - placement: placements.BOTTOM, - sectionName: props.programCategory?.displayName, - }), + getMeta: (props: Object) => { + const { firstStageMetaData, programCategory } = props; + + return { + section: AOCsectionKey, + placement: placements.BOTTOM, + sectionName: firstStageMetaData + ? `${firstStageMetaData.stage.name} - ${programCategory?.displayName}` + : programCategory?.displayName, + }; + }, }; return categoryOptionsSettings; }; const getAOCSettingsFn = () => ({ - hideAOC: ({ programId, newDashboardConfig }) => { - const { stages: stagesMap } = getProgramThrowIfNotFound(programId); + hideAOC: ({ programId }) => { + const { stages: stagesMap, useFirstStageDuringRegistration } = getProgramThrowIfNotFound(programId); /* Show AOC selection if: - - There are any program stages in the program with “Auto-generate" event and NOT “Open data entry form after enrollment”. - - There are multiple program stages with "Auto-generate" event and "Open data entry form after enrollment" - (in this scenario we are currently generating events for the program stages that are not the first one) - - Using the old dashboard and we have a stage with auto generate event. + - There are any program stages in the program with “Auto-generate" + - The "Show first stage on registration page" is selected for the program */ const stages = [...stagesMap.values()]; - const usingNewDashboardForProgram = shouldUseNewDashboard( - newDashboardConfig.userDataStore, - newDashboardConfig.dataStore, - newDashboardConfig.temp, - programId, - ); - - const shouldShowAOC = (!usingNewDashboardForProgram && stages.some(stage => stage.autoGenerateEvent)) || - stages.some(stage => stage.autoGenerateEvent && !stage.openAfterEnrollment) || - stages.filter(stage => stage.autoGenerateEvent && stage.openAfterEnrollment).length > 1; + const shouldShowAOC = stages.some(stage => stage.autoGenerateEvent) || useFirstStageDuringRegistration; return !shouldShowAOC; }, @@ -336,7 +331,9 @@ type FinalTeiDataEntryProps = { orgUnitId: string, onUpdateDataEntryField: Function, onUpdateFormFieldAsync: Function, - onUpdateFormField: Function + onUpdateFormField: Function, + firstStageMetaData?: ?{ stage: ProgramStage }, + formFoundation: RenderFoundation, }; // final step before the generic dataEntry is inserted class FinalEnrollmentDataEntry extends React.Component { @@ -355,14 +352,21 @@ class FinalEnrollmentDataEntry extends React.Component { }; render() { - const { enrollmentMetadata, ...passOnProps } = this.props; + const { enrollmentMetadata, firstStageMetaData, ...passOnProps } = this.props; + return ( // $FlowFixMe[cannot-spread-inexact] automated comment - + firstStageMetaData ? ( + + ) : ( + + ) ); } } @@ -385,6 +389,8 @@ type PreEnrollmentDataEntryProps = { onStartAsyncUpdateField: Function, onGetUnsavedAttributeValues?: ?Function, teiId?: ?string, + firstStageMetaData?: ?{ stage: ProgramStage }, + formFoundation: RenderFoundation, }; class PreEnrollmentDataEntryPure extends React.PureComponent { @@ -409,18 +415,18 @@ export class EnrollmentDataEntryComponent extends React.Component) => { - const { programId, orgUnit } = this.props; - this.props.onUpdateField(...args, programId, orgUnit); + const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; + this.props.onUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); } handleUpdateDataEntryField = (...args: Array) => { - const { programId, orgUnit } = this.props; - this.props.onUpdateDataEntryField(...args, programId, orgUnit); + const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; + this.props.onUpdateDataEntryField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); } handleStartAsyncUpdateField = (...args: Array) => { - const { programId, orgUnit } = this.props; - this.props.onStartAsyncUpdateField(...args, programId, orgUnit); + const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props; + this.props.onStartAsyncUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation); } render() { @@ -432,6 +438,7 @@ export class EnrollmentDataEntryComponent extends React.Component ({ - newDashboardConfig: useNewDashboard, -}); - const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ onUpdateDataEntryField: ( innerAction: ReduxAction, data: any, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { - dispatch(updateDataEntryFieldBatch(innerAction, programId, orgUnit)); + dispatch(updateDataEntryFieldBatch(innerAction, programId, orgUnit, stage, formFoundation)); }, onUpdateField: ( innerAction: ReduxAction, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { - dispatch(updateFieldBatch(innerAction, programId, orgUnit)); + dispatch(updateFieldBatch(innerAction, programId, orgUnit, stage, formFoundation)); }, onStartAsyncUpdateField: ( innerAction: ReduxAction, @@ -31,9 +32,11 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ itemId: string, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { const onAsyncUpdateSuccess = (successInnerAction: ReduxAction) => - asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit); + asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit, stage, formFoundation); const onAsyncUpdateError = (errorInnerAction: ReduxAction) => errorInnerAction; dispatch(startAsyncUpdateFieldForNewEnrollment(innerAction, onAsyncUpdateSuccess, onAsyncUpdateError)); @@ -41,5 +44,5 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ }); // $FlowFixMe -export const EnrollmentDataEntry = connect(mapStateToProps, mapDispatchToProps)(EnrollmentDataEntryComponent); +export const EnrollmentDataEntry = connect(() => ({}), mapDispatchToProps)(EnrollmentDataEntryComponent); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js new file mode 100644 index 0000000000..c1b0b328b0 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.js @@ -0,0 +1,231 @@ +// @flow +import i18n from '@dhis2/d2-i18n'; +import { DataEntry } from '../../../DataEntry'; +import { + withInternalChangeHandler, + withLabel, + withFocusSaver, + DateField, + PolygonField, + CoordinateField, + withCalculateMessages, + withDisplayMessages, + withFilterProps, + withDefaultFieldContainer, + withDefaultShouldUpdateInterface, + TrueOnlyField, + orientations, +} from '../../../FormFields/New'; +import { placements } from '../../../DataEntry/constants/placements.const'; +import { withDataEntryFieldIfApplicable } from '../../../DataEntry/dataEntryField/withDataEntryFieldIfApplicable'; +import { sectionKeysForFirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry.constants'; +import labelTypeClasses from './fieldLabels.module.css'; +import { withCleanUp } from './withCleanUp'; +import { getEventDateValidatorContainers } from './fieldValidators/eventDate.validatorContainersGetter'; +import { stageMainDataIds } from './getDataEntryPropsToInclude'; + +const overrideMessagePropNames = { + errorMessage: 'validationError', +}; + +const getCalendarAnchorPosition = (formHorizontal: ?boolean) => (formHorizontal ? 'center' : 'left'); + +const baseComponentStyles = { + labelContainerStyle: { + flexBasis: 200, + }, + inputContainerStyle: { + flexBasis: 150, + }, +}; +const baseComponentStylesVertical = { + labelContainerStyle: { + width: 150, + }, + inputContainerStyle: { + width: 150, + }, +}; + +function defaultFilterProps(props: Object) { + const { formHorizontal, fieldOptions, validationError, modified, ...passOnProps } = props; + return passOnProps; +} + +const getBaseComponentProps = (props: Object) => ({ + fieldOptions: props.fieldOptions, + formHorizontal: props.formHorizontal, + styles: props.formHorizontal ? baseComponentStylesVertical : baseComponentStyles, +}); + +const createComponentProps = (props: Object, componentProps: Object) => ({ + ...getBaseComponentProps(props), + ...componentProps, +}); + +const getOrientation = (formHorizontal: ?boolean) => (formHorizontal ? orientations.VERTICAL : orientations.HORIZONTAL); + +const pointComponent = withCalculateMessages(overrideMessagePropNames)( + withFocusSaver()( + withDefaultFieldContainer()( + withDefaultShouldUpdateInterface()( + withLabel({ + onGetUseVerticalOrientation: (props: Object) => props.formHorizontal, + onGetCustomFieldLabeClass: (props: Object) => + `${props.fieldOptions && props.fieldOptions.fieldLabelMediaBasedClass} ${labelTypeClasses.coordinateLabel}`, + })( + withDisplayMessages()( + withInternalChangeHandler()( + withFilterProps(defaultFilterProps)(CoordinateField), + ), + ), + ), + ), + ), + ), +); + +const polygonComponent = withCalculateMessages(overrideMessagePropNames)( + withFocusSaver()( + withDefaultFieldContainer()( + withDefaultShouldUpdateInterface()( + withLabel({ + onGetUseVerticalOrientation: (props: Object) => props.formHorizontal, + onGetCustomFieldLabeClass: (props: Object) => + `${props.fieldOptions && props.fieldOptions.fieldLabelMediaBasedClass} ${labelTypeClasses.coordinateLabel}`, + })( + withDisplayMessages()( + withInternalChangeHandler()( + withFilterProps(defaultFilterProps)(PolygonField), + ), + ), + ), + ), + ), + ), +); + +const getStageGeometrySettings = () => ({ + isApplicable: (props: Object) => { + const featureType = props.firstStageMetaData?.stage?.stageForm?.featureType; + return ['Polygon', 'Point'].includes(featureType); + }, + getComponent: (props: Object) => { + const featureType = props.firstStageMetaData?.stage?.stageForm?.featureType; + if (featureType === 'Polygon') { + return polygonComponent; + } + return pointComponent; + }, + getComponentProps: (props: Object) => { + const featureType = props.firstStageMetaData?.stage?.stageForm?.featureType; + if (featureType === 'Polygon') { + return createComponentProps(props, { + width: props && props.formHorizontal ? 150 : 350, + label: i18n.t('Area'), + dialogLabel: i18n.t('Area'), + required: false, + orientation: getOrientation(props.formHorizontal), + }); + } + + return createComponentProps(props, { + width: props && props.formHorizontal ? 150 : 350, + label: i18n.t('Coordinate'), + dialogLabel: i18n.t('Coordinate'), + required: false, + orientation: getOrientation(props.formHorizontal), + shrinkDisabled: props.formHorizontal, + }); + }, + getPropName: () => stageMainDataIds.GEOMETRY, + getValidatorContainers: () => [], + getMeta: () => ({ + section: sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO, + }), +}); + +const getCompleteFieldSettingsFn = () => { + const completeComponent = + withCalculateMessages(overrideMessagePropNames)( + withFocusSaver()( + withDefaultFieldContainer()( + withDefaultShouldUpdateInterface()( + withLabel({ + onGetUseVerticalOrientation: (props: Object) => props.formHorizontal, + onGetCustomFieldLabeClass: (props: Object) => + `${props.fieldOptions && props.fieldOptions.fieldLabelMediaBasedClass} ${labelTypeClasses.trueOnlyLabel}`, + })( + withDisplayMessages()( + withInternalChangeHandler()( + withFilterProps(defaultFilterProps)(TrueOnlyField), + ), + ), + ), + ), + ), + ), + ); + const completeSettings = { + isApplicable: (props: Object) => props.firstStageMetaData && props.firstStageMetaData.stage?.stageForm, + getComponent: () => completeComponent, + getComponentProps: (props: Object) => createComponentProps(props, { + label: i18n.t('Complete event'), + id: 'complete', + }), + getPropName: () => stageMainDataIds.COMPLETE, + getValidatorContainers: () => [], + getMeta: () => ({ + placement: placements.BOTTOM, + section: sectionKeysForFirstStageDataEntry.STATUS, + }), + getPassOnFieldData: () => true, + }; + + return completeSettings; +}; + +const getReportDateSettingsFn = () => { + const reportDateComponent = + withCalculateMessages(overrideMessagePropNames)( + withFocusSaver()( + withDefaultFieldContainer()( + withDefaultShouldUpdateInterface()( + withLabel({ + onGetUseVerticalOrientation: (props: Object) => props.formHorizontal, + onGetCustomFieldLabeClass: (props: Object) => + `${props.fieldOptions && props.fieldOptions.fieldLabelMediaBasedClass} ${labelTypeClasses.dateLabel}`, + })( + withDisplayMessages()( + withInternalChangeHandler()( + withFilterProps(defaultFilterProps)(DateField), + ), + ), + ), + ), + ), + ), + ); + const reportDateSettings = { + isApplicable: (props: Object) => props.firstStageMetaData?.stage?.stageForm, + getComponent: () => reportDateComponent, + getComponentProps: (props: Object) => createComponentProps(props, { + width: props && props.formHorizontal ? 150 : '100%', + label: props.firstStageMetaData?.stage?.stageForm?.getLabel('occurredAt'), + required: true, + calendarWidth: props.formHorizontal ? 250 : 350, + popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal), + }), + getPropName: () => stageMainDataIds.OCCURRED_AT, + getValidatorContainers: () => getEventDateValidatorContainers(), + getMeta: () => ({ + section: sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO, + }), + }; + + return reportDateSettings; +}; + +const StageLocationHOC = withDataEntryFieldIfApplicable(getStageGeometrySettings())(withCleanUp()(DataEntry)); +const CompleteHOC = withDataEntryFieldIfApplicable(getCompleteFieldSettingsFn())(StageLocationHOC); +export const FirstStageDataEntry = withDataEntryFieldIfApplicable(getReportDateSettingsFn())(CompleteHOC); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.constants.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.constants.js new file mode 100644 index 0000000000..1abc295aa9 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.constants.js @@ -0,0 +1,7 @@ +// @flow + +export const sectionKeysForFirstStageDataEntry = { + ENROLLMENT: 'enrollment', + STATUS: 'status', + STAGE_BASIC_INFO: 'stageBasicInfo', +}; 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 new file mode 100644 index 0000000000..b831b9f197 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.container.js @@ -0,0 +1,28 @@ +// @flow +import React from 'react'; +import type { Props } from './EnrollmentWithFirstStageDataEntry.types'; +import { FirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry.component'; +import { useDataEntrySections } from './hooks'; +import { Section } from '../../../../metaData'; + +const getSectionId = sectionId => + (sectionId === Section.MAIN_SECTION_ID ? `${Section.MAIN_SECTION_ID}-stage` : sectionId); + +export const EnrollmentWithFirstStageDataEntry = (props: Props) => { + const { firstStageMetaData, ...passOnProps } = props; + const { + stage: { stageForm: firstStageFormFoundation, name: stageName }, + } = firstStageMetaData; + // $FlowFixMe[incompatible-type] + const [[firstSectionId]] = firstStageFormFoundation.sections; + const beforeSectionId = getSectionId(firstSectionId); + const dataEntrySections = useDataEntrySections(stageName, beforeSectionId); + + return ( + + ); +}; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js new file mode 100644 index 0000000000..6c2a054867 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.types.js @@ -0,0 +1,9 @@ +// @flow +import type { ProgramStage, RenderFoundation } from '../../../../metaData'; + +export type Props = { + firstStageMetaData: { + stage: ProgramStage, + }, + formFoundation: RenderFoundation, +}; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldLabels.module.css b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldLabels.module.css new file mode 100644 index 0000000000..faa712abaf --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldLabels.module.css @@ -0,0 +1,45 @@ +.booleanLabel { + padding-top: 13px; +} + +.trueOnlyLabel { + padding-top: 13px; +} +.noteLabel { + padding-top: 13px; +} + +.textLabel { + padding-top: 13px; +} + +.ageLabel { + padding-top: 13px; +} + +.coordinateLabel { + padding-top: 13px; +} +.polygonLabel { + padding-top: 4px; +} + +.dateLabel { + padding-top: 13px; +} + +.dateTimeLabel { + padding-top: 13px; +} + +.imageLabel { + padding-top: 9px; +} + +.fileLabel { + padding-top: 9px; +} + +.orgUnitLabel { + padding-top: 3px; +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js new file mode 100644 index 0000000000..3cb0a56c19 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/eventDate.validatorContainersGetter.js @@ -0,0 +1,26 @@ +// @flow +import { hasValue } from 'capture-core-utils/validators/form'; +import i18n from '@dhis2/d2-i18n'; +import { isValidDate } from '../../../../../utils/validators/form'; + +const preValidateDate = (value?: ?string) => { + if (!value) { + return true; + } + + return isValidDate(value); +}; + +export const getEventDateValidatorContainers = () => { + const validatorContainers = [ + { + validator: hasValue, + message: i18n.t('A value is required'), + }, + { + validator: preValidateDate, + message: i18n.t('Please provide a valid date'), + }, + ]; + return validatorContainers; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/index.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/index.js new file mode 100644 index 0000000000..5399d49334 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/fieldValidators/index.js @@ -0,0 +1,3 @@ +// @flow + +export { getEventDateValidatorContainers } from './eventDate.validatorContainersGetter'; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/getDataEntryPropsToInclude.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/getDataEntryPropsToInclude.js new file mode 100644 index 0000000000..aef6ec85cf --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/getDataEntryPropsToInclude.js @@ -0,0 +1,39 @@ +// @flow +import type { RenderFoundation } from '../../../../metaData'; +import { getEventDateValidatorContainers } from './fieldValidators'; +import { getConvertGeometryIn, convertGeometryOut, convertStatusIn, convertStatusOut } from '../../converters'; + +export const stageMainDataIds = { + OCCURRED_AT: 'stageOccurredAt', + COMPLETE: 'stageComplete', + GEOMETRY: 'stageGeometry', +}; + +const stageMainDataRulesEngineIds = { + OCCURRED_AT: 'occurredAt', + COMPLETE: 'complete', + GEOMETRY: 'geometry', +}; + +export const convertToRulesEngineIds = (id: string) => stageMainDataRulesEngineIds[id]; + +export const getDataEntryPropsToInclude = (formFoundation: RenderFoundation) => [ + { + id: stageMainDataIds.OCCURRED_AT, + type: 'DATE', + validatorContainers: getEventDateValidatorContainers(), + }, + { + clientId: stageMainDataIds.COMPLETE, + dataEntryId: stageMainDataIds.COMPLETE, + onConvertIn: convertStatusIn, + onConvertOut: convertStatusOut, + }, + { + clientId: stageMainDataIds.GEOMETRY, + dataEntryId: stageMainDataIds.GEOMETRY, + onConvertIn: getConvertGeometryIn(formFoundation), + onConvertOut: convertGeometryOut, + featureType: formFoundation.featureType, + }, +]; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/index.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/index.js new file mode 100644 index 0000000000..83d6e81265 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/index.js @@ -0,0 +1,3 @@ +// @flow + +export { useDataEntrySections } from './useDataEntrySections'; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.js new file mode 100644 index 0000000000..3aab5bd6d2 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.js @@ -0,0 +1,34 @@ +// @flow +import { useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { placements } from '../../../../DataEntry/constants/placements.const'; +import { sectionKeysForFirstStageDataEntry } from '../EnrollmentWithFirstStageDataEntry.constants'; +import { AOCsectionKey } from '../../../../DataEntryDhis2Helpers'; + +export const useDataEntrySections = (stageName: string, beforeSectionId: string) => + useMemo( + () => ({ + [sectionKeysForFirstStageDataEntry.ENROLLMENT]: { + placement: placements.TOP, + name: i18n.t('Enrollment'), + }, + [sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO]: { + beforeSectionId, + placement: placements.BEFORE_METADATA_BASED_SECTION, + name: i18n.t('{{ stageName }} - Basic info', { + stageName, + }), + }, + [AOCsectionKey]: { + beforeSectionId, + placement: placements.BEFORE_METADATA_BASED_SECTION, + }, + [sectionKeysForFirstStageDataEntry.STATUS]: { + placement: placements.BOTTOM, + name: i18n.t('{{ stageName }} - Status', { + stageName, + }), + }, + }), + [stageName, beforeSectionId], + ); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/index.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/index.js new file mode 100644 index 0000000000..9570ba1aba --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/index.js @@ -0,0 +1,4 @@ +// @flow + +export { EnrollmentWithFirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry.container'; +export { getDataEntryPropsToInclude, stageMainDataIds, convertToRulesEngineIds } from './getDataEntryPropsToInclude'; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/withCleanUp.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/withCleanUp.js new file mode 100644 index 0000000000..5b2cc24c0e --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/withCleanUp.js @@ -0,0 +1,21 @@ +// @flow +import * as React from 'react'; + +const getCleanUpHOC = (InnerComponent: React.ComponentType) => + (props: Object) => { + const { + dataEntryFieldRef, + firstStageMetaData, + ...passOnProps + } = props; + + return ( + + ); + }; + +export const withCleanUp = () => + (InnerComponent: React.ComponentType) => + getCleanUpHOC(InnerComponent); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js index c59fa959ee..f84452c7fe 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actionBatchs.js @@ -8,7 +8,7 @@ import type { } from '@dhis2/rules-engine-javascript'; import { getApplicableRuleEffectsForTrackerProgram, updateRulesEffects } from '../../../../rules'; import { rulesExecutedPostUpdateField } from '../../../DataEntry/actions/dataEntry.actions'; -import type { TrackerProgram, RenderFoundation } from '../../../../metaData'; +import { TrackerProgram, RenderFoundation, ProgramStage } from '../../../../metaData'; import { startRunRulesPostUpdateField } from '../../../DataEntry'; import { startRunRulesOnUpdateForNewEnrollment } from './enrollment.actions'; @@ -18,25 +18,42 @@ export const batchActionTypes = { UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH: 'UpdateDataEntryFieldNewEnrollmentActionBatch', }; -export const runRulesOnUpdateFieldBatch = ( +export const runRulesOnUpdateFieldBatch = ({ + program, + formId, + dataEntryId, + itemId, + orgUnit, + enrollmentData, + attributeValues, + extraActions = [], + uid, + stage, + formFoundation, + currentEvent, +}: { program: TrackerProgram, - foundation: RenderFoundation, formId: string, dataEntryId: string, itemId: string, orgUnit: OrgUnit, enrollmentData?: Enrollment, attributeValues?: TEIValues, - extraActions: Array> = [], + extraActions: Array>, uid: string, -) => { + stage?: ProgramStage, + formFoundation?: RenderFoundation, + currentEvent?: {[id: string]: any}, +}) => { const effects = getApplicableRuleEffectsForTrackerProgram({ program, + stage, orgUnit, + currentEvent, enrollmentData, attributeValues, + formFoundation, }); - return batchActions([ updateRulesEffects(effects, formId), rulesExecutedPostUpdateField(dataEntryId, itemId, uid), @@ -48,6 +65,8 @@ export const updateDataEntryFieldBatch = ( innerAction: ReduxAction, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { const { dataEntryId, itemId } = innerAction.payload; const uid = uuid(); @@ -55,7 +74,7 @@ export const updateDataEntryFieldBatch = ( return batchActions([ innerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit), + startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit, stage, formFoundation), ], batchActionTypes.UPDATE_DATA_ENTRY_FIELD_NEW_ENROLLMENT_ACTION_BATCH); }; @@ -63,6 +82,8 @@ export const updateFieldBatch = ( innerAction: ReduxAction, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { const { dataEntryId, itemId } = innerAction.payload; const uid = uuid(); @@ -70,7 +91,7 @@ export const updateFieldBatch = ( return batchActions([ innerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit), + startRunRulesOnUpdateForNewEnrollment(innerAction.payload, uid, programId, orgUnit, stage, formFoundation), ], batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH); }; @@ -80,12 +101,14 @@ export const asyncUpdateSuccessBatch = ( itemId: string, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => { const uid = uuid(); return batchActions([ innerAction, startRunRulesPostUpdateField(dataEntryId, itemId, uid), - startRunRulesOnUpdateForNewEnrollment({ ...innerAction.payload, dataEntryId, itemId }, uid, programId, orgUnit), + startRunRulesOnUpdateForNewEnrollment({ ...innerAction.payload, dataEntryId, itemId }, uid, programId, orgUnit, stage, formFoundation), ], batchActionTypes.UPDATE_FIELD_NEW_ENROLLMENT_ACTION_BATCH); }; diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js index b259a5821c..68d8a8cb7e 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/enrollment.actions.js @@ -1,6 +1,7 @@ // @flow import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { actionCreator, actionPayloadAppender } from '../../../../actions/actions.utils'; +import { ProgramStage, RenderFoundation } from '../../../../metaData'; export const actionTypes = { START_RUN_RULES_ON_UPDATE: 'StartRunRulesOnUpdateForNewEnrollment', @@ -11,9 +12,11 @@ export const startRunRulesOnUpdateForNewEnrollment = ( uid: string, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, ) => actionCreator(actionTypes.START_RUN_RULES_ON_UPDATE)( - { innerPayload: payload, uid, programId, orgUnit }); + { innerPayload: payload, uid, programId, orgUnit, stage, formFoundation }); export const startAsyncUpdateFieldForNewEnrollment = ( innerAction: ReduxAction, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/open.actionBatchs.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/open.actionBatchs.js index f83897ebfa..2507d1c474 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/open.actionBatchs.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/actions/open.actionBatchs.js @@ -2,21 +2,26 @@ import { batchActions } from 'redux-batched-actions'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { getApplicableRuleEffectsForTrackerProgram, updateRulesEffects } from '../../../../rules'; -import type { TrackerProgram } from '../../../../metaData'; +import type { ProgramStage, TrackerProgram, RenderFoundation } from '../../../../metaData'; import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; import { loadNewDataEntry } from '../../../DataEntry/actions/dataEntryLoadNew.actions'; import { openDataEntryForNewEnrollment } from './open.actions'; -import { getEnrollmentDateValidatorContainer, getIncidentDateValidatorContainer, getCategoryOptionsValidatorContainers } from '../fieldValidators'; +import { + getEnrollmentDateValidatorContainer, + getIncidentDateValidatorContainer, + getCategoryOptionsValidatorContainers, +} from '../fieldValidators'; import { convertGeometryOut } from '../../converters'; import { convertDateObjectToDateFormatString } from '../../../../utils/converters/date'; import { addFormData } from '../../../D2Form/actions/form.actions'; import type { ProgramCategory } from '../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; +import { getDataEntryPropsToInclude } from '../EnrollmentWithFirstStageDataEntry'; const itemId = 'newEnrollment'; type DataEntryPropsToInclude = Array; -const dataEntryPropsToInclude: DataEntryPropsToInclude = [ +const enrollmentDataEntryPropsToInclude: DataEntryPropsToInclude = [ { id: 'enrolledAt', type: 'DATE', @@ -48,7 +53,9 @@ export const openDataEntryForNewEnrollmentBatchAsync = async ({ extraDataEntryProps = [], formValues, clientValues, + firstStage, programCategory, + formFoundation, }: { program: TrackerProgram, orgUnit: OrgUnit, @@ -57,30 +64,39 @@ export const openDataEntryForNewEnrollmentBatchAsync = async ({ extraDataEntryProps?: Array, formValues: { [key: string]: any }, clientValues: { [key: string]: any }, + firstStage?: ProgramStage, programCategory?: ProgramCategory, + formFoundation: RenderFoundation, }) => { const formId = getDataEntryKey(dataEntryId, itemId); - - if (programCategory && programCategory.categories) { - dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ + const addFormDataActions = addFormData(`${dataEntryId}-${itemId}`, formValues); + const firstStageDataEntryPropsToInclude = firstStage && getDataEntryPropsToInclude(firstStage.stageForm); + const dataEntryPropsToInclude = [ + ...enrollmentDataEntryPropsToInclude, + ...extraDataEntryProps, + ...(firstStageDataEntryPropsToInclude || []), + ...(programCategory && programCategory.categories ? programCategory.categories.map(category => ({ id: `attributeCategoryOptions-${category.id}`, type: 'TEXT', - validatorContainers: getCategoryOptionsValidatorContainers({ categories: programCategory.categories }, category.id), - }))); - } + validatorContainers: + getCategoryOptionsValidatorContainers({ categories: programCategory.categories }, category.id), + })) : []), + ]; + const dataEntryActions = loadNewDataEntry( dataEntryId, itemId, - [...dataEntryPropsToInclude, ...extraDataEntryProps], + dataEntryPropsToInclude, { enrolledAt: convertDateObjectToDateFormatString(new Date()) }, ); - const addFormDataActions = addFormData(`${dataEntryId}-${itemId}`, formValues); const effects = getApplicableRuleEffectsForTrackerProgram({ program, orgUnit, + stage: firstStage, attributeValues: clientValues, + formFoundation, }); return batchActions([ diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js b/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js index f5b11285e8..9bc4da634a 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/epics/enrollment.epics.js @@ -4,9 +4,11 @@ import { ofType } from 'redux-observable'; import { map } from 'rxjs/operators'; import { batchActionTypes, runRulesOnUpdateFieldBatch } from '../actions/enrollment.actionBatchs'; import { actionTypes } from '../actions/enrollment.actions'; -import { getTrackerProgramThrowIfNotFound } from '../../../../metaData'; -import { getCurrentClientValues, getCurrentClientMainData, type FieldData } from '../../../../rules'; +import { getTrackerProgramThrowIfNotFound, ProgramStage, RenderFoundation, Section } from '../../../../metaData'; +import { getCurrentClientMainData, type FieldData } from '../../../../rules'; import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import { convertFormToClient } from '../../../../converters'; +import { stageMainDataIds, convertToRulesEngineIds } from '../EnrollmentWithFirstStageDataEntry'; type Context = { dataEntryId: string, @@ -14,31 +16,59 @@ type Context = { uid: string, programId: string, orgUnit: OrgUnit, + stage?: ProgramStage, + formFoundation: RenderFoundation, } +const splitCurrentClientMainData = (stage, currentClientMainData) => { + if (!stage) { + return { currentEnrollmentValues: currentClientMainData, currentEventMainData: {} }; + } + return Object.keys(currentClientMainData).reduce((acc, id) => { + const stageMainDataId = Object.keys(stageMainDataIds).find(key => stageMainDataIds[key] === id); + if (stageMainDataId) { + acc.currentEventMainData = { + ...acc.currentEventMainData, + [convertToRulesEngineIds(stageMainDataId)]: currentClientMainData[id], + }; + } else { + acc.currentEnrollmentValues = { ...acc.currentEnrollmentValues, [id]: currentClientMainData[id] }; + } + return acc; + }, { currentEnrollmentValues: {}, currentEventMainData: {} }); +}; + const runRulesOnEnrollmentUpdate = (store: ReduxStore, context: Context, fieldData?: ?FieldData, searchActions?: any = []) => { const state = store.value; - const { programId, dataEntryId, itemId, orgUnit, uid } = context; + const { programId, dataEntryId, itemId, orgUnit, uid, stage, formFoundation } = context; const formId = getDataEntryKey(dataEntryId, itemId); const program = getTrackerProgramThrowIfNotFound(programId); - const foundation = program.enrollment.enrollmentForm; - const currentTEIValues = getCurrentClientValues(state, foundation, formId, fieldData); - const currentEnrollmentValues = - getCurrentClientMainData(state, itemId, dataEntryId, foundation); + const currentFormData = state.formsValues[formId] || {}; + const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFormToClient); + const attributeValues = convertedValues[Section.groups.ENROLLMENT]; + const currentEventValues = convertedValues[Section.groups.EVENT] || {}; + const currentClientMainData = + getCurrentClientMainData(state, itemId, dataEntryId, formFoundation) || {}; + const { currentEnrollmentValues, currentEventMainData } + = splitCurrentClientMainData(state, currentClientMainData); + const currentEvent = stage + ? { ...currentEventValues, ...currentEventMainData, programStageId: stage.id } : undefined; - return runRulesOnUpdateFieldBatch( + return runRulesOnUpdateFieldBatch({ program, - foundation, formId, dataEntryId, itemId, orgUnit, - currentEnrollmentValues, - currentTEIValues ?? undefined, - searchActions, + enrollmentData: currentEnrollmentValues, + attributeValues, + currentEvent, + extraActions: searchActions, uid, - ); + stage, + formFoundation: stage ? formFoundation : undefined, + }); }; @@ -53,6 +83,8 @@ export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = (action$: InputObser programId, orgUnit, innerPayload, + stage, + formFoundation, } = action.payload; const { @@ -66,6 +98,8 @@ export const runRulesOnEnrollmentDataEntryFieldUpdateEpic = (action$: InputObser uid, programId, orgUnit, + stage, + formFoundation, }); })); @@ -75,7 +109,7 @@ export const runRulesOnEnrollmentFieldUpdateEpic = (action$: InputObservable, st map(actionBatch => actionBatch.payload.find(action => action.type === actionTypes.START_RUN_RULES_ON_UPDATE)), map((action) => { - const { innerPayload: payload, searchActions, uid, programId, orgUnit } = action.payload; + const { innerPayload: payload, searchActions, uid, programId, orgUnit, stage, formFoundation } = action.payload; const { dataEntryId, itemId, elementId, value, uiState } = payload; const fieldData: FieldData = { @@ -90,6 +124,8 @@ export const runRulesOnEnrollmentFieldUpdateEpic = (action$: InputObservable, st dataEntryId, itemId, uid, + stage, + formFoundation, }, fieldData, searchActions); }), ); diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.actions.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.actions.js index 4e74581537..2a54784b45 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.actions.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.actions.js @@ -5,6 +5,23 @@ export const enrollmentRegistrationEntryActionTypes = { TRACKER_PROGRAM_REGISTRATION_ENTRY_INITIALISATION_START: 'StartInitForEnrollmentRegistrationForm', }; -export const startNewEnrollmentDataEntryInitialisation = ({ selectedOrgUnit, selectedScopeId, dataEntryId, formValues, clientValues, programCategory }: Object) => - actionCreator(enrollmentRegistrationEntryActionTypes.TRACKER_PROGRAM_REGISTRATION_ENTRY_INITIALISATION_START)({ selectedOrgUnit, selectedScopeId, dataEntryId, formValues, clientValues, programCategory }); - +export const startNewEnrollmentDataEntryInitialisation = ({ + selectedOrgUnit, + selectedScopeId, + dataEntryId, + formValues, + clientValues, + programCategory, + firstStage, + formFoundation, +}: Object) => + actionCreator(enrollmentRegistrationEntryActionTypes.TRACKER_PROGRAM_REGISTRATION_ENTRY_INITIALISATION_START)({ + selectedOrgUnit, + selectedScopeId, + dataEntryId, + formValues, + clientValues, + programCategory, + firstStage, + formFoundation, + }); 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 6d624541be..623a16abce 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 @@ -8,23 +8,26 @@ import { useLifecycle } from './hooks'; import { useCurrentOrgUnitInfo } from '../../../hooks/useCurrentOrgUnitInfo'; import { useRulesEngineOrgUnit } from '../../../hooks'; import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges'; -import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm'; export const EnrollmentRegistrationEntry: ComponentType = ({ selectedScopeId, id, saveButtonText, trackedEntityInstanceAttributes, + onSave, ...passOnProps }) => { const orgUnitId = useCurrentOrgUnitInfo().id; const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); - const { teiId, ready, skipDuplicateCheck } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); const { + teiId, + ready, + skipDuplicateCheck, + firstStageMetaData, formId, - registrationMetaData: enrollmentMetadata, + enrollmentMetadata, formFoundation, - } = useMetadataForRegistrationForm({ selectedScopeId }); + } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); const isUserInteractionInProgress: boolean = useSelector( state => @@ -35,7 +38,6 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ ); const trackedEntityTypeNameLC = enrollmentMetadata?.trackedEntityType?.name.toLocaleLowerCase() ?? ''; - const isSavingInProgress = useSelector(({ possibleDuplicates, newPage }) => possibleDuplicates.isLoading || possibleDuplicates.isUpdating || !!newPage.uid); @@ -46,6 +48,7 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ return ( = ({ orgUnit={orgUnit} isUserInteractionInProgress={isUserInteractionInProgress} isSavingInProgress={isSavingInProgress} + onSave={() => onSave(formFoundation, firstStageMetaData)} /> ); }; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.epics.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.epics.js index 5737a4988b..44e25f9bc4 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.epics.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.epics.js @@ -15,7 +15,16 @@ export const startNewEnrollmentDataEntrySelfInitialisationEpic = (action$: Input action$.pipe( ofType(enrollmentRegistrationEntryActionTypes.TRACKER_PROGRAM_REGISTRATION_ENTRY_INITIALISATION_START), pluck('payload'), - switchMap(({ selectedOrgUnit, selectedScopeId: programId, dataEntryId, formValues, clientValues, programCategory }) => { + switchMap(({ + selectedOrgUnit, + selectedScopeId: programId, + dataEntryId, + formValues, + clientValues, + programCategory, + firstStage, + formFoundation, + }) => { if (selectedOrgUnit) { let trackerProgram: ?TrackerProgram; try { @@ -33,7 +42,9 @@ export const startNewEnrollmentDataEntrySelfInitialisationEpic = (action$: Input dataEntryId, formValues, clientValues, + firstStage, programCategory, + formFoundation, }); return from(openEnrollmentPromise); 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 463fbecc06..f1924c00b3 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 @@ -6,7 +6,7 @@ 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 } from '../../../metaData'; +import { RenderFoundation, ProgramStage } from '../../../metaData'; export type OwnProps = $ReadOnly<{| id: string, @@ -21,6 +21,7 @@ export type OwnProps = $ReadOnly<{| skipDuplicateCheck?: ?boolean, trackedEntityInstanceAttributes?: Array, saveButtonText: (trackedEntityName: string) => string, + firstStageMetaData?: ?{ stage: ?ProgramStage }, |}>; type ContainerProps = {| diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildFirstStageRegistration.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildFirstStageRegistration.js new file mode 100644 index 0000000000..4bd04e8f62 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildFirstStageRegistration.js @@ -0,0 +1,32 @@ +// @flow +import { useMemo } from 'react'; +import { getProgramAndStageForProgram } from '../../../../metaData'; +import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/useProgramFromIndexedDB'; + +export const useBuildFirstStageRegistration = (programId: string, skip: boolean = false) => { + const { program, isLoading } = useProgramFromIndexedDB(programId, { enabled: !skip }); + + const firstStage = useMemo(() => { + if (!isLoading && program?.useFirstStageDuringRegistration) { + const { programStages } = program; + const programStagesWithAccess = programStages + .filter((stage) => { + const access = { + read: stage.access.data.read, + write: stage.access.data.write, + }; + return access.write; + }) + .sort((a, b) => a.sortOrder - b.sortOrder); + return programStagesWithAccess[0]?.id; + } + return null; + }, [program, isLoading]); + + const firstStageMetaData = useMemo( + () => (firstStage && programId ? getProgramAndStageForProgram(programId, firstStage) : null), + [firstStage, programId], + ); + + return { loading: isLoading, firstStageMetaData }; +}; 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 a7b645c87c..0c4a280bf6 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 @@ -8,8 +8,10 @@ import { useLocationQuery } from '../../../../utils/routing'; import { useScopeInfo } from '../../../../hooks/useScopeInfo'; import { useFormValues } from './index'; import type { InputAttribute } from './useFormValues'; +import { useBuildFirstStageRegistration } from './useBuildFirstStageRegistration'; import { useMetadataForRegistrationForm } from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; import { useCategoryCombinations } from '../../../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; +import { useMergeFormFoundationsIfApplicable } from './useMergeFormFoundationsIfApplicable'; export const useLifecycle = ( selectedScopeId: string, @@ -24,7 +26,14 @@ export const useLifecycle = ( const ready = useSelector(({ dataEntries }) => !!dataEntries[dataEntryId]) && !!orgUnit; const searchTerms = useSelector(({ searchDomain }) => searchDomain.currentSearchInfo.currentSearchTerms); const { scopeType } = useScopeInfo(selectedScopeId); - const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); + const { firstStageMetaData } = useBuildFirstStageRegistration(programId, scopeType !== scopeTypes.TRACKER_PROGRAM); + const { + formId, + registrationMetaData: enrollmentMetadata, + formFoundation: enrollmentFormFoundation, + } = useMetadataForRegistrationForm({ selectedScopeId }); + + const { formFoundation } = useMergeFormFoundationsIfApplicable(enrollmentFormFoundation, firstStageMetaData); const { programCategory } = useCategoryCombinations(programId, scopeType !== scopeTypes.TRACKER_PROGRAM); const { formValues, clientValues, formValuesReadyRef } = useFormValues({ program, @@ -43,7 +52,8 @@ export const useLifecycle = ( dataEntryReadyRef.current === false && formValuesReadyRef.current === true && orgUnit && - scopeType === scopeTypes.TRACKER_PROGRAM + scopeType === scopeTypes.TRACKER_PROGRAM && + formFoundation ) { dataEntryReadyRef.current = true; dispatch( @@ -54,10 +64,32 @@ export const useLifecycle = ( formValues, clientValues, programCategory, + firstStage: firstStageMetaData?.stage, + formFoundation, }), ); } - }, [scopeType, dataEntryId, selectedScopeId, orgUnit, formValuesReadyRef, formValues, clientValues, programCategory, dispatch]); + }, [ + formFoundation, + scopeType, + dataEntryId, + selectedScopeId, + orgUnit, + formValuesReadyRef, + formValues, + clientValues, + programCategory, + firstStageMetaData, + dispatch, + ]); - return { teiId, ready, skipDuplicateCheck: !!teiId }; + return { + teiId, + ready, + skipDuplicateCheck: !!teiId, + firstStageMetaData, + formId, + enrollmentMetadata, + formFoundation, + }; }; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useMergeFormFoundationsIfApplicable.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useMergeFormFoundationsIfApplicable.js new file mode 100644 index 0000000000..77235be403 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useMergeFormFoundationsIfApplicable.js @@ -0,0 +1,72 @@ +// @flow +import { useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { RenderFoundation, Section, ProgramStage } from '../../../../metaData'; + +const addElements = (section, newSection) => + Array.from(section.elements.entries()) + .map(entry => entry[1]) + .forEach((element) => { + // $FlowFixMe[prop-missing] section is missing in FormFieldPluginConfig + element.section = newSection; + newSection.addElement(element); + }); + +const getSectionId = sectionId => + (sectionId === Section.MAIN_SECTION_ID ? `${Section.MAIN_SECTION_ID}-stage` : sectionId); + +export const useMergeFormFoundationsIfApplicable = ( + enrollmentFormFoundation?: ?RenderFoundation, + firstStageMetaData?: ?{ stage: ?ProgramStage }, +) => { + const enrollmentSectionsSize = enrollmentFormFoundation?.sections.size; + + return useMemo(() => { + const firstStageFormFoundation = firstStageMetaData?.stage?.stageForm; + if (!enrollmentFormFoundation) { + return { formFoundation: null }; + } + + if (!firstStageFormFoundation || enrollmentSectionsSize === 0) { + return { formFoundation: enrollmentFormFoundation }; + } + + const stageName = firstStageMetaData?.stage?.name; + const { id, name, access, description, featureType, validationStrategy } = enrollmentFormFoundation; + const renderFoundation = new RenderFoundation((o) => { + o.id = id; + o.name = name; + o.access = access; + o.description = description; + o.featureType = featureType; + o.validationStrategy = validationStrategy; + }); + + enrollmentFormFoundation.sections.forEach(section => renderFoundation.addSection(section)); + + firstStageFormFoundation.sections.forEach((section) => { + const isMainSection = section.id === Section.MAIN_SECTION_ID; + const newSection = new Section((o) => { + o.id = getSectionId(section.id); + o.name = isMainSection + ? i18n.t('{{ stageName }} - Details', { + stageName, + }) + : i18n.t('{{ stageName }} - {{ sectionName }}', { + stageName, + sectionName: section.name, + }); + o.group = Section.groups.EVENT; + o.customForm = section.customForm; + o.showContainer = section.showContainer; + o.open = section.open; + o.visible = section.visible; + o.displayDescription = section.displayDescription; + }); + addElements(section, newSection); + renderFoundation.addSection(newSection); + }); + + return { formFoundation: renderFoundation }; + }, [enrollmentFormFoundation, firstStageMetaData, enrollmentSectionsSize]); +}; 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 83ab589551..a727db9ec2 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,3 +1,7 @@ // @flow +import { ProgramStage, RenderFoundation } from '../../../../../../metaData'; -export type SaveForDuplicateCheck = () => void; +export type SaveForDuplicateCheck = ( + formFoundation?: RenderFoundation, + firstStageMetaData?: { stage: ProgramStage }, +) => void; diff --git a/src/core_modules/capture-core/components/DataEntries/converters/converters.js b/src/core_modules/capture-core/components/DataEntries/converters/converters.js index 64b17bc514..a0efa43b3a 100644 --- a/src/core_modules/capture-core/components/DataEntries/converters/converters.js +++ b/src/core_modules/capture-core/components/DataEntries/converters/converters.js @@ -1,14 +1,15 @@ // @flow import type { RenderFoundation } from '../../../metaData'; -export function convertGeometryOut(dataEntryValue: any, foundation: RenderFoundation) { - if (!dataEntryValue || !['Polygon', 'Point'].includes(foundation.featureType)) return null; +export function convertGeometryOut(dataEntryValue: any, foundation: RenderFoundation, customFeatureType: string) { + const featureType = customFeatureType || foundation.featureType; + if (!dataEntryValue || !['Polygon', 'Point'].includes(featureType)) return null; let coordinates = dataEntryValue; - if (foundation.featureType === 'Point') { + if (featureType === 'Point') { coordinates = [dataEntryValue.longitude, dataEntryValue.latitude]; } return { - type: foundation.featureType, + type: featureType, coordinates, }; } diff --git a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js index b554fb3b2b..c0c818c491 100644 --- a/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js +++ b/src/core_modules/capture-core/components/DataEntry/DataEntry.component.js @@ -10,7 +10,7 @@ import { getDataEntryKey } from './common/getDataEntryKey'; import { StickyOnScroll } from '../Sticky/StickyOnScroll.component'; import { Section } from '../Section/Section.component'; import { SectionHeaderSimple } from '../Section/SectionHeaderSimple.component'; -import { FieldSection } from './FieldSection.component'; +import { Field } from './Field.component'; const styles = theme => ({ loadingContainer: { @@ -69,11 +69,17 @@ const styles = theme => ({ verticalOutputsContainer: { marginBottom: theme.typography.pxToRem(10), }, - dataEntryFieldSectionContainer: { + dataEntrySectionContainer: { marginBottom: spacers.dp16, }, }); +type DataEntrySection = { + placement: $Values, + name: string, + beforeSectionId?: string +}; + type FieldContainer = { field: React.Element, placement: $Values, @@ -120,7 +126,7 @@ type Props = { dataEntryId: string, itemId: string, ) => void, - dataEntrySections?: { [string]: {name: string, placement: $Values}}, + dataEntrySections?: { [string]: DataEntrySection }, dataEntryFieldRef: any, onAddNote?: ?Function, onOpenAddRelationship?: ?Function, @@ -159,22 +165,34 @@ class DataEntryPlain extends React.Component { }; } + hasPlacement = ( + dataEntrySection: DataEntrySection, + placement: $Values, + beforeSectionId?: string, + ) => + dataEntrySection.placement === placement && + (dataEntrySection.placement !== placements.BEFORE_METADATA_BASED_SECTION || + (dataEntrySection.placement === placements.BEFORE_METADATA_BASED_SECTION && + dataEntrySection.beforeSectionId === beforeSectionId + ) + ); + handleUpdateFieldAsync = (...args) => { this.props.onUpdateFormFieldAsync(...args, this.props.id, this.props.itemId); } - getFieldSectionsWithPlacement(placement: $Values) { + getSectionsWithPlacement(placement: $Values, beforeSectionId?: string) { const fields = this.props.fields || []; const sections = this.props.dataEntrySections || {}; return this.props.dataEntrySections ? Object.keys(this.props.dataEntrySections).reduce((accSections, sectionKey) => { const section = sections[sectionKey]; - if (section.placement === placement) { + if (this.hasPlacement(section, placement, beforeSectionId)) { const sectionFields = fields .filter(fieldContainer => fieldContainer.section === sectionKey); const sectionFieldsContainer = sectionFields.map((fieldContainer, index, array) => ( - { accSections.push(
{ : []; } - renderDataEntryFieldsByPlacement = (placement: $Values) => { + renderDataEntryFieldsByPlacement = (placement: $Values, beforeSectionId?: string) => { const fields = this.props.fields || []; const fieldFilter = this.props.formHorizontal ? fieldHorizontalFilter(placement) : fieldVerticalFilter(placement); const fieldsByPlacement = fields ? fields .filter(fieldFilter) - .map((fieldContainer, index, array) => ( ( { : []; if (!this.props.formHorizontal) { - return [...fieldsByPlacement, ...this.getFieldSectionsWithPlacement(placement)]; + return [...fieldsByPlacement, ...this.getSectionsWithPlacement(placement, beforeSectionId)]; } return fieldsByPlacement; } @@ -258,6 +276,9 @@ class DataEntryPlain extends React.Component { validationAttempted={completionAttempted || saveAttempted} onUpdateField={this.handleUpdateField} onUpdateFieldAsync={this.handleUpdateFieldAsync} + getCustomContent={beforeSectionId => ( + this.renderDataEntryFieldsByPlacement(placements.BEFORE_METADATA_BASED_SECTION, beforeSectionId) + )} {...passOnProps} /> ); diff --git a/src/core_modules/capture-core/components/DataEntry/FieldSection.component.js b/src/core_modules/capture-core/components/DataEntry/Field.component.js similarity index 91% rename from src/core_modules/capture-core/components/DataEntry/FieldSection.component.js rename to src/core_modules/capture-core/components/DataEntry/Field.component.js index a67eb08b33..d53831c878 100644 --- a/src/core_modules/capture-core/components/DataEntry/FieldSection.component.js +++ b/src/core_modules/capture-core/components/DataEntry/Field.component.js @@ -24,7 +24,7 @@ type Props = { ...CssClasses } -const FieldSectionPlain = (props: Props) => { +const FieldPlain = (props: Props) => { const { formHorizontal, index, fieldContainer, total, classes } = props; const className = !formHorizontal ? index % 2 !== 0 && classes.evenNumbers : null; @@ -44,4 +44,4 @@ const FieldSectionPlain = (props: Props) => { ); }; -export const FieldSection = withStyles(styles)(FieldSectionPlain); +export const Field = withStyles(styles)(FieldPlain); diff --git a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js index 5fb8fccaec..88e7bb9208 100644 --- a/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js +++ b/src/core_modules/capture-core/components/DataEntry/actions/dataEntryLoad.utils.js @@ -17,7 +17,8 @@ type DataEntryPropToIncludeSpecial = {| clientId: string, dataEntryId: string, onConvertIn: (value: any) => any, - onConvertOut: (dataEntryValue: any, prevValue: any, foundation: RenderFoundation) => any, + onConvertOut: (dataEntryValue: any, foundation: RenderFoundation, customFeatureType: string) => any, + featureType?: string, validatorContainers?: ?Array, |}; @@ -30,7 +31,11 @@ export function getDataEntryMeta(dataEntryPropsToInclude: Array { const error = useSelector(({ activePage }) => activePage.viewEventLoadError?.error); const { loading, event } = useEvent(eventId); const { program: programId, programStage: stageId, trackedEntity: teiId, enrollment: enrollmentId } = event; - const { orgUnitId, eventId: urlEventId } = useLocationQuery(); + const { orgUnitId, eventId: urlEventId, initMode } = useLocationQuery(); useEffect(() => { if (!urlEventId) { @@ -73,11 +73,20 @@ export const EnrollmentEditEventPage = () => { orgUnitId={orgUnitId} eventId={eventId} error={error} + initMode={initMode} /> ) : ; }; -const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, enrollmentId, orgUnitId, eventId }: Props) => { +const EnrollmentEditEventPageWithContextPlain = ({ + programId, + stageId, + teiId, + enrollmentId, + orgUnitId, + eventId, + initMode, +}: Props) => { const history = useHistory(); const dispatch = useDispatch(); @@ -98,9 +107,14 @@ const EnrollmentEditEventPageWithContextPlain = ({ programId, stageId, teiId, en const onAddNew = () => { history.push(`/new?${buildUrlQueryString({ programId, orgUnitId, teiId })}`); }; - const onCancelEditEvent = () => { - history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); - }; + const onCancelEditEvent = useCallback((isScheduled: boolean) => { + if (isScheduled) { + history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); + } + if (initMode) { + history.push(`/enrollmentEventEdit?${buildUrlQueryString({ eventId, orgUnitId })}`); + } + }, [initMode, enrollmentId, eventId, orgUnitId, history]); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); 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 59ff58c190..8e16b09325 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 @@ -21,7 +21,7 @@ export type PlainProps = {| onGoBack: () => void, onEnrollmentError: (message: string) => void, onEnrollmentSuccess: () => void, - onCancelEditEvent: () => void, + onCancelEditEvent: (isScheduled: boolean) => void, onHandleScheduleSave: (eventData: Object) => void, pageStatus: string, eventStatus?: string, @@ -35,4 +35,5 @@ export type Props = {| enrollmentId: string, orgUnitId: string, eventId: string, + initMode?: string, |}; 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 74753cb8b0..e716f71741 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,5 +1,5 @@ // @flow -import type { RenderFoundation } from '../../../../metaData'; +import type { ProgramStage, RenderFoundation } from '../../../../metaData'; import { actionCreator } from '../../../../actions/actions.utils'; import { effectMethods } from '../../../../trackerOffline'; @@ -41,23 +41,26 @@ export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => ); // with enrollment -export const startSavingNewTrackedEntityInstanceWithEnrollment = (formFoundation: RenderFoundation, teiId: string, uid: string) => +export const startSavingNewTrackedEntityInstanceWithEnrollment = (formFoundation: RenderFoundation, teiId: string, uid: string, firstStage?: ProgramStage) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START)({ formFoundation, teiId, + firstStage, uid, }); export const saveNewTrackedEntityInstanceWithEnrollment = ({ candidateForRegistration, - redirectToEnrollmentEventNew, + redirectTo, uid, stageId, + eventIndex, }: { candidateForRegistration: any, - redirectToEnrollmentEventNew: boolean, + redirectTo: string, uid: string, stageId?: string, + eventIndex: number, }) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE)( { ...candidateForRegistration }, @@ -70,7 +73,7 @@ export const saveNewTrackedEntityInstanceWithEnrollment = ({ }, commit: { type: registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_COMPLETED, - meta: { redirectToEnrollmentEventNew, stageId, uid }, + meta: { redirectTo, stageId, uid, eventIndex }, }, rollback: { type: registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED, 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 7a8cf76cc6..cf14636450 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 @@ -179,7 +179,9 @@ const RegistrationDataEntryPlain = ({ onSaveWithEnrollment(formFoundation)} + onSave={(customFormFoundation, firstStageMetaData) => + onSaveWithEnrollment(customFormFoundation, firstStageMetaData?.stage) + } saveButtonText={(trackedEntityTypeNameLC: string) => i18n.t('Save {{trackedEntityTypeName}}', { trackedEntityTypeName: trackedEntityTypeNameLC, interpolation: { escapeValue: false }, 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 bdebbcb962..41f5a864d5 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 @@ -30,9 +30,9 @@ export const RegistrationDataEntry: ComponentType = ({ [dispatch]); const dispatchOnSaveWithEnrollment = useCallback( - (formFoundation) => { + (formFoundation, firstStage) => { const uid = uuid(); - dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(formFoundation, teiId, uid)); + dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(formFoundation, teiId, uid, firstStage)); }, [dispatch, teiId]); 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 7fab7c500f..004adb75f7 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 @@ -3,46 +3,30 @@ import { ofType } from 'redux-observable'; import { pipe } from 'capture-core-utils'; import { flatMap, map } from 'rxjs/operators'; import { of, EMPTY } from 'rxjs'; -import moment from 'moment'; +import { FEATURETYPE, dataEntryKeys } from 'capture-core/constants'; import { registrationFormActionTypes, saveNewTrackedEntityInstance, saveNewTrackedEntityInstanceWithEnrollment, } from './RegistrationDataEntry.actions'; -import { getTrackerProgramThrowIfNotFound, dataElementTypes } from '../../../../metaData'; +import { getTrackerProgramThrowIfNotFound, dataElementTypes, Section } from '../../../../metaData'; import { navigateToEnrollmentOverview, } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { convertFormToClient, convertClientToServer } from '../../../../converters'; -import { FEATURETYPE } from '../../../../constants'; import { buildUrlQueryString, shouldUseNewDashboard } from '../../../../utils/routing'; -import { convertCategoryOptionsToServer } from '../../../../converters/clientToServer'; +import { + deriveAutoGenerateEvents, + deriveFirstStageDuringRegistrationEvent, + getStageWithOpenAfterEnrollment, + standardGeoJson, + PAGES, +} from './helpers'; const convertFn = pipe(convertFormToClient, convertClientToServer); const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); -const standardGeoJson = (geometry) => { - if (!geometry) { - return undefined; - } - if (Array.isArray(geometry)) { - return { - type: 'Polygon', - coordinates: geometry, - }; - } else if (geometry.longitude && geometry.latitude) { - return { - type: 'Point', - coordinates: [geometry.longitude, geometry.latitude], - }; - } - return undefined; -}; - -const getStageWithOpenAfterEnrollment = stages => - [...stages.values()].find(({ openAfterEnrollment }) => openAfterEnrollment); - const deriveAttributesFromFormValues = (formValues = {}) => Object.keys(formValues) .filter(key => !geometryType(key)) @@ -53,64 +37,6 @@ const deriveGeometryFromFormValues = (formValues = {}) => .filter(key => geometryType(key)) .reduce((acc, currentKey) => (standardGeoJson(formValues[currentKey])), undefined); - -const deriveEvents = ({ - stages, - enrolledAt, - occurredAt, - programId, - orgUnitId, - redirectToEnrollmentEventNew, - redirectToStageId, - attributeCategoryOptions, -}) => { - // in case we have a program that does not have an incident date (occurredAt), such as Malaria case diagnosis, - // we want the incident to default to enrollmentDate (enrolledAt) - const sanitizedOccurredAt = occurredAt || enrolledAt; - return [...stages.values()] - .filter(({ id }) => (redirectToEnrollmentEventNew && id !== redirectToStageId) || !redirectToEnrollmentEventNew) - .filter(({ autoGenerateEvent }) => autoGenerateEvent) - .map(({ - id: programStage, - reportDateToUse: reportDateToUseInActiveStatus, - generatedByEnrollmentDate: generateScheduleDateByEnrollmentDate, - openAfterEnrollment, - minDaysFromStart, - }) => { - const dateToUseInActiveStatus = - reportDateToUseInActiveStatus === 'enrolledAt' ? enrolledAt : sanitizedOccurredAt; - const dateToUseInScheduleStatus = generateScheduleDateByEnrollmentDate ? enrolledAt : sanitizedOccurredAt; - const eventAttributeCategoryOptions = {}; - if (attributeCategoryOptions) { - eventAttributeCategoryOptions.attributeCategoryOptions = convertCategoryOptionsToServer(attributeCategoryOptions); - } - const eventInfo = - openAfterEnrollment - ? - { - status: 'ACTIVE', - occurredAt: convertFn(dateToUseInActiveStatus, dataElementTypes.DATE), - scheduledAt: convertFn(dateToUseInActiveStatus, dataElementTypes.DATE), - } - : - { - status: 'SCHEDULE', - // for schedule type of events we want to add the standard interval days to the date - scheduledAt: moment(convertFn(dateToUseInScheduleStatus, dataElementTypes.DATE)) - .add(minDaysFromStart, 'days') - .format('YYYY-MM-DD'), - }; - - return { - ...eventInfo, - ...eventAttributeCategoryOptions, - programStage, - program: programId, - orgUnit: orgUnitId, - }; - }); -}; - export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable, store: ReduxStore) => action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START), @@ -154,9 +80,11 @@ 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 { dataStore, userDataStore, temp } = store.value.useNewDashboard; - const fieldsValue = dataEntriesFieldsValue['newPageDataEntryId-newEnrollment'] || {}; + 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) @@ -167,29 +95,47 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( return acc; }, {}); const { trackedEntityType, stages } = getTrackerProgramThrowIfNotFound(programId); - const values = formsValues['newPageDataEntryId-newEnrollment'] || {}; - const stageWithOpenAfterEnrollment = getStageWithOpenAfterEnrollment(stages); - const redirectToEnrollmentEventNew = - shouldUseNewDashboard(userDataStore, dataStore, temp, programId) && stageWithOpenAfterEnrollment !== undefined; - const events = deriveEvents({ + const currentFormData = formsValues[formId] || {}; + const shouldRedirect = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); + const { stageWithOpenAfterEnrollment, redirectTo } = getStageWithOpenAfterEnrollment( + stages, + firstStageMetadata, + 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, - redirectToEnrollmentEventNew, - redirectToStageId: stageWithOpenAfterEnrollment?.id, + firstStageMetadata, attributeCategoryOptions, }); - const { formFoundation, teiId: trackedEntity, uid } = action.payload; - const formServerValues = formFoundation?.convertValues(values, convertFn); + const allEventsToBeCreated = firstStageDuringRegistrationEvent + ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] + : autoGenerateEvents; + const eventIndex = allEventsToBeCreated.findIndex( + eventsToBeCreated => eventsToBeCreated.programStage === stageWithOpenAfterEnrollment?.id, + ); return saveNewTrackedEntityInstanceWithEnrollment({ candidateForRegistration: { trackedEntities: [ { - geometry: deriveGeometryFromFormValues(values), + geometry: deriveGeometryFromFormValues(currentFormData), enrollments: [ { geometry: standardGeoJson(geometry), @@ -199,7 +145,7 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( orgUnit: orgUnitId, attributes: deriveAttributesFromFormValues(formServerValues), status: 'ACTIVE', - events, + events: allEventsToBeCreated, }, ], orgUnit: orgUnitId, @@ -208,7 +154,8 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( }, ], }, - redirectToEnrollmentEventNew, + redirectTo, + eventIndex, stageId: stageWithOpenAfterEnrollment?.id, uid, }); @@ -223,7 +170,12 @@ export const completeSavingNewTrackedEntityInstanceWithEnrollmentEpic = ( action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_COMPLETED), flatMap((action) => { - const { payload: { bundleReport: { typeReportMap } }, meta } = action; + const { + payload: { + bundleReport: { typeReportMap }, + }, + meta: { uid, redirectTo, stageId, eventIndex }, + } = action; const { currentSelections: { orgUnitId, programId }, newPage, @@ -231,19 +183,31 @@ export const completeSavingNewTrackedEntityInstanceWithEnrollmentEpic = ( const { uid: stateUid } = newPage || {}; const teiId = typeReportMap.TRACKED_ENTITY.objectReports[0].uid; const enrollmentId = typeReportMap.ENROLLMENT.objectReports[0].uid; + const eventId = typeReportMap.EVENT.objectReports?.[eventIndex]?.uid; - if (stateUid !== meta.uid) { + if (stateUid !== uid) { return EMPTY; } - if (meta?.redirectToEnrollmentEventNew) { + if (redirectTo === PAGES.enrollmentEventNew) { history.push( - `/enrollmentEventNew?${buildUrlQueryString({ + `/${redirectTo}?${buildUrlQueryString({ programId, orgUnitId, teiId, enrollmentId, - stageId: meta?.stageId, + stageId, + })}`, + ); + return EMPTY; + } + + if (redirectTo === PAGES.enrollmentEventEdit) { + history.push( + `/${redirectTo}?${buildUrlQueryString({ + eventId, + orgUnitId, + initMode: dataEntryKeys.EDIT, })}`, ); return EMPTY; 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 new file mode 100644 index 0000000000..a38c30e4e5 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js @@ -0,0 +1,79 @@ +// @flow +import { pipe } from 'capture-core-utils'; +import moment from 'moment'; +import { dataElementTypes, ProgramStage } from '../../../../../metaData'; +import { convertFormToClient, convertClientToServer } from '../../../../../converters'; +import { convertCategoryOptionsToServer } from '../../../../../converters/clientToServer'; + +const convertFn = pipe(convertFormToClient, convertClientToServer); + +const ignoreAutoGenerateIfApplicable = (stage, firstStageDuringRegistrationEvent) => + !firstStageDuringRegistrationEvent || firstStageDuringRegistrationEvent.id !== stage.id; + +export const deriveAutoGenerateEvents = ({ + stages, + enrolledAt, + occurredAt, + programId, + orgUnitId, + firstStageMetadata, + attributeCategoryOptions, +}: { + stages: Map, + enrolledAt: string, + occurredAt: string, + programId: string, + orgUnitId: string, + 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, + // we want the incident to default to enrollmentDate (enrolledAt) + const sanitizedOccurredAt = occurredAt || enrolledAt; + + // $FlowFixMe[missing-annot] + return [...stages.values()] + .filter(({ autoGenerateEvent }) => autoGenerateEvent) + .filter(stage => ignoreAutoGenerateIfApplicable(stage, firstStageMetadata)) + .map( + ({ + id: programStage, + reportDateToUse: reportDateToUseInActiveStatus, + generatedByEnrollmentDate: generateScheduleDateByEnrollmentDate, + openAfterEnrollment, + minDaysFromStart, + }) => { + const dateToUseInActiveStatus = + reportDateToUseInActiveStatus === 'enrolledAt' ? enrolledAt : sanitizedOccurredAt; + const dateToUseInScheduleStatus = generateScheduleDateByEnrollmentDate + ? enrolledAt + : sanitizedOccurredAt; + const eventAttributeCategoryOptions = {}; + if (attributeCategoryOptions) { + eventAttributeCategoryOptions.attributeCategoryOptions = + convertCategoryOptionsToServer(attributeCategoryOptions); + } + const eventInfo = openAfterEnrollment + ? { + status: 'ACTIVE', + occurredAt: convertFn(dateToUseInActiveStatus, dataElementTypes.DATE), + scheduledAt: convertFn(dateToUseInActiveStatus, dataElementTypes.DATE), + } + : { + status: 'SCHEDULE', + // for schedule type of events we want to add the standard interval days to the date + scheduledAt: moment(convertFn(dateToUseInScheduleStatus, dataElementTypes.DATE)) + .add(minDaysFromStart, 'days') + .format('YYYY-MM-DD'), + }; + + return { + ...eventInfo, + ...eventAttributeCategoryOptions, + programStage, + program: programId, + orgUnit: orgUnitId, + }; + }, + ); +}; 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 new file mode 100644 index 0000000000..bf40f45135 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js @@ -0,0 +1,55 @@ +// @flow +import { pipe } from 'capture-core-utils'; +import { dataElementTypes, ProgramStage } from '../../../../../metaData'; +import { convertFormToClient, convertClientToServer } from '../../../../../converters'; +import { convertCategoryOptionsToServer } from '../../../../../converters/clientToServer'; +import { convertStatusOut } from '../../../../DataEntries'; +import { standardGeoJson } from './standardGeoJson'; + +const convertFn = pipe(convertFormToClient, convertClientToServer); + +export const deriveFirstStageDuringRegistrationEvent = ({ + firstStageMetadata, + programId, + orgUnitId, + currentEventValues, + fieldsValue, + attributeCategoryOptions, +}: { + firstStageMetadata: ProgramStage, + programId: string, + orgUnitId: string, + currentEventValues?: { [id: string]: any }, + fieldsValue: { [id: string]: any }, + attributeCategoryOptions: { [categoryId: string]: string } | string, +}) => { + if (!firstStageMetadata) { + return null; + } + const { enrolledAt, stageComplete, stageOccurredAt, stageGeometry } = fieldsValue; + + const eventAttributeCategoryOptions = attributeCategoryOptions + ? { attributeCategoryOptions: convertCategoryOptionsToServer(attributeCategoryOptions) } + : {}; + + const event = { + status: convertStatusOut(stageComplete), + geometry: standardGeoJson(stageGeometry), + occurredAt: convertFn(stageOccurredAt, dataElementTypes.DATE), + scheduledAt: convertFn(enrolledAt, dataElementTypes.DATE), + programStage: firstStageMetadata.id, + program: programId, + orgUnit: orgUnitId, + ...eventAttributeCategoryOptions, + }; + + const dataValues = currentEventValues ? Object.keys(currentEventValues).reduce((acc, dataElement) => { + acc.push({ dataElement, value: currentEventValues[dataElement] }); + return acc; + }, []) : undefined; + + if (dataValues) { + return { ...event, dataValues }; + } + return event; +}; 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 new file mode 100644 index 0000000000..26bd03daad --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js @@ -0,0 +1,45 @@ +// @flow +import { ProgramStage } from '../../../../../metaData'; + +export const PAGES = { + enrollmentEventNew: 'enrollmentEventNew', + enrollmentEventEdit: 'enrollmentEventEdit', + enrollmentDashboard: 'enrollmentDashboard', +}; + +// an event can be created either during first stage registration or autogenerated +// when the event will be created redirect to enrollmentEventEdit +// when the event will not be created redirect to enrollmentEventNew +export const getStageWithOpenAfterEnrollment = ( + stages: Map, + firstStageMetadata: ProgramStage, + shouldRedirect: boolean, +) => { + const stagesArray = [...stages.values()]; + const [firstStageWithOpenAfterEnrollment] = stagesArray.filter(({ openAfterEnrollment }) => openAfterEnrollment); + + const redirectTo = (() => { + if (shouldRedirect && firstStageWithOpenAfterEnrollment) { + // event will be created during first stage registration + if ( + firstStageMetadata && + firstStageMetadata.id === firstStageWithOpenAfterEnrollment.id + ) { + return PAGES.enrollmentEventEdit; + } + // event will be autogenerated + if ( + stagesArray.find(stage => stage.autoGenerateEvent && stage.id === firstStageWithOpenAfterEnrollment.id) + ) { + return PAGES.enrollmentEventEdit; + } + return PAGES.enrollmentEventNew; + } + return PAGES.enrollmentDashboard; + })(); + + return { + stageWithOpenAfterEnrollment: firstStageWithOpenAfterEnrollment, + redirectTo, + }; +}; diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/index.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/index.js new file mode 100644 index 0000000000..fe7c884a9e --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/index.js @@ -0,0 +1,5 @@ +// @flow +export { deriveFirstStageDuringRegistrationEvent } from './deriveFirstStageDuringRegistrationEvent'; +export { deriveAutoGenerateEvents } from './deriveAutoGenerateEvents'; +export { getStageWithOpenAfterEnrollment, PAGES } from './getStageWithOpenAfterEnrollment'; +export { standardGeoJson } from './standardGeoJson'; diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/standardGeoJson.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/standardGeoJson.js new file mode 100644 index 0000000000..4342ab42c8 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/standardGeoJson.js @@ -0,0 +1,19 @@ +// @flow + +export const standardGeoJson = (geometry: Array | { longitude: number, latitude: number }) => { + if (!geometry) { + return undefined; + } + if (Array.isArray(geometry)) { + return { + type: 'Polygon', + coordinates: geometry, + }; + } else if (geometry.longitude && geometry.latitude) { + return { + type: 'Point', + coordinates: [geometry.longitude, geometry.latitude], + }; + } + return undefined; +}; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index 6561b81018..5542c3e8d4 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -394,7 +394,7 @@ type Props = { }, theme: Theme, dataEntryId: string, - onCancelEditEvent?: () => void, + onCancelEditEvent?: (isScheduled: boolean) => void, eventStatus?: string, enrollmentId?: string, isCompleted?: boolean, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.js index eb4e6b0829..0a701bf622 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.js @@ -103,7 +103,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props): any => ({ cancelEditEventDataEntry(), ...(isScheduled ? [] : [setCurrentDataEntry(props.dataEntryId, dataEntryKeys.VIEW)]), ])); - isScheduled && onCancelEditEvent && onCancelEditEvent(); + onCancelEditEvent && onCancelEditEvent(isScheduled); }, onDelete: () => { const { enrollmentId } = props; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js index 16536ce27c..8dbc06e0c6 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js @@ -6,7 +6,7 @@ export type Props = {| programStage: ProgramStage, eventStatus?: string, onGoBack: () => void, - onCancelEditEvent: () => void, + onCancelEditEvent: (isScheduled: boolean) => void, onHandleScheduleSave: (eventData: Object) =>void, orgUnitId: string, programId: string, diff --git a/src/core_modules/capture-core/hooks/useEnrollmentEditEventPageMode.js b/src/core_modules/capture-core/hooks/useEnrollmentEditEventPageMode.js index 80b36d6210..c3a62fbb53 100644 --- a/src/core_modules/capture-core/hooks/useEnrollmentEditEventPageMode.js +++ b/src/core_modules/capture-core/hooks/useEnrollmentEditEventPageMode.js @@ -1,13 +1,22 @@ // @flow import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; import { dataEntryKeys } from 'capture-core/constants'; import { statusTypes } from '../events/statusTypes'; +import { useLocationQuery } from '../utils/routing'; export const useEnrollmentEditEventPageMode = (eventStatus?: string) => { const showEditEvent = useSelector(({ viewEventPage }) => viewEventPage?.eventDetailsSection?.showEditEvent); + const { initMode } = useLocationQuery(); - if (eventStatus === statusTypes.SCHEDULE || eventStatus === statusTypes.OVERDUE) { - return { currentPageMode: dataEntryKeys.EDIT }; - } - return { currentPageMode: showEditEvent ? dataEntryKeys.EDIT : dataEntryKeys.VIEW }; + return useMemo(() => { + if (initMode) { + return { currentPageMode: initMode }; + } + + if (eventStatus === statusTypes.SCHEDULE || eventStatus === statusTypes.OVERDUE) { + return { currentPageMode: dataEntryKeys.EDIT }; + } + return { currentPageMode: showEditEvent ? dataEntryKeys.EDIT : dataEntryKeys.VIEW }; + }, [initMode, showEditEvent, eventStatus]); }; diff --git a/src/core_modules/capture-core/metaData/DataElement/DataElement.js b/src/core_modules/capture-core/metaData/DataElement/DataElement.js index c336e0f4f5..5555d4842e 100644 --- a/src/core_modules/capture-core/metaData/DataElement/DataElement.js +++ b/src/core_modules/capture-core/metaData/DataElement/DataElement.js @@ -11,6 +11,7 @@ import { OptionSet } from '../OptionSet/OptionSet'; import type { Unique } from './Unique'; import { dataElementTypes } from './dataElementTypes'; import type { CachedAttributeValue } from '../../storageControllers'; +import type { Section } from '../RenderFoundation'; // eslint-disable-next-line no-use-before-define export type ConvertFn = (value: any, type: $Keys, element: DataElement) => any; @@ -37,6 +38,7 @@ export class DataElement { _searchable: ?boolean; _url: ?string; _attributeValues: Array + _section: ?Section; constructor(initFn: ?(_this: DataElement) => void) { this._displayInReports = true; @@ -60,6 +62,13 @@ export class DataElement { return this._name; } + set section(section: ?Section) { + this._section = section; + } + get section(): ?Section { + return this._section; + } + set code(code: string) { this._code = code; } diff --git a/src/core_modules/capture-core/metaData/Program/Program.js b/src/core_modules/capture-core/metaData/Program/Program.js index e3d9645332..88ff443877 100644 --- a/src/core_modules/capture-core/metaData/Program/Program.js +++ b/src/core_modules/capture-core/metaData/Program/Program.js @@ -25,6 +25,7 @@ export class Program { _programRuleVariables: Array; _icon: Icon | void; _displayFrontPageList: boolean; + _useFirstStageDuringRegistration: boolean; _onlyEnrollOnce: boolean; constructor(initFn: ?(_this: Program) => void) { @@ -115,6 +116,14 @@ export class Program { return this._displayFrontPageList; } + set useFirstStageDuringRegistration(useFirstStageDuringRegistration: boolean) { + this._useFirstStageDuringRegistration = useFirstStageDuringRegistration; + } + + get useFirstStageDuringRegistration() { + return this._useFirstStageDuringRegistration; + } + set onlyEnrollOnce(onlyEnrollOnce: boolean) { this._onlyEnrollOnce = onlyEnrollOnce; } diff --git a/src/core_modules/capture-core/metaData/Program/ProgramStage.js b/src/core_modules/capture-core/metaData/Program/ProgramStage.js index 1c501643ca..403a2df333 100644 --- a/src/core_modules/capture-core/metaData/Program/ProgramStage.js +++ b/src/core_modules/capture-core/metaData/Program/ProgramStage.js @@ -20,6 +20,7 @@ export class ProgramStage { _openAfterEnrollment: boolean; _allowGenerateNextVisit: boolean; _generatedByEnrollmentDate: boolean; + _repeatable: boolean; _hideDueDate: boolean; _reportDateToUse: string; _minDaysFromStart: number; @@ -156,4 +157,12 @@ export class ProgramStage { get programRules(): Array { return this._programRules; } + + set repeatable(repeatable: boolean) { + this._repeatable = repeatable; + } + + get repeatable(): boolean { + return this._repeatable; + } } diff --git a/src/core_modules/capture-core/metaData/RenderFoundation/RenderFoundation.js b/src/core_modules/capture-core/metaData/RenderFoundation/RenderFoundation.js index 8c11e94144..9fdf44a12f 100644 --- a/src/core_modules/capture-core/metaData/RenderFoundation/RenderFoundation.js +++ b/src/core_modules/capture-core/metaData/RenderFoundation/RenderFoundation.js @@ -4,7 +4,7 @@ import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import isFunction from 'd2-utilizr/lib/isFunction'; import { validationStrategies, validationStrategiesAsArray } from './renderFoundation.const'; -import type { Section } from './Section'; +import { Section } from './Section'; import type { ConvertFn } from '../DataElement/DataElement'; import type { Access } from '../Access'; import { convertDataElementsValues } from '../helpers'; @@ -163,4 +163,20 @@ export class RenderFoundation { const dataElements = this.getElements(); return convertDataElementsValues(values, dataElements, onConvert); } + + convertAndGroupBySection(currentFormData: {[id: string]: any}, onConvert: ConvertFn) { + const metaElements = [...this.getElements().values()]; + + return Object.keys(currentFormData).reduce((acc, id) => { + const metaElement = metaElements.find(o => o.id === id); + const rawValue = currentFormData[id]; + const convertedValue = metaElement ? metaElement.convertValue(rawValue, onConvert) : rawValue; + const group = metaElement?.section?.group; + if (group) { + acc[group] = { ...acc[group], [id]: convertedValue }; + return acc; + } + return { ...acc, [id]: convertedValue }; + }, {}); + } } diff --git a/src/core_modules/capture-core/metaData/RenderFoundation/Section.js b/src/core_modules/capture-core/metaData/RenderFoundation/Section.js index d312f59842..411067757b 100644 --- a/src/core_modules/capture-core/metaData/RenderFoundation/Section.js +++ b/src/core_modules/capture-core/metaData/RenderFoundation/Section.js @@ -15,6 +15,11 @@ export class Section { DATA_ELEMENT_NOT_FOUND: 'Data element was not found', }; + static groups = { + ENROLLMENT: 'ENROLLMENT', + EVENT: 'EVENT', + } + _id: string; _name: string; _displayDescription: string; @@ -24,6 +29,7 @@ export class Section { _elements: Map; _showContainer: boolean; _customForm: ?CustomForm; + _group: string; constructor(initFn: (_this: Section) => void) { this._visible = true; @@ -90,6 +96,13 @@ export class Section { return this._elements; } + set group(group: string) { + this._group = group; + } + get group(): string { + return this._group; + } + addElement(element: DataElement | FormFieldPluginConfig) { if (!this.elements.has(element.id)) { this.elements.set(element.id, element); diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js index 3fa73e1130..18e56e1869 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/DataElementFactory.js @@ -15,6 +15,7 @@ import { DataElementUnique, dataElementUniqueScope, dataElementTypes, + Section, } from '../../../../metaData'; import { OptionSetFactory } from '../../../common/factory'; import { convertFormToClient, convertClientToServer } from '../../../../converters'; @@ -37,8 +38,9 @@ export class DataElementFactory { 'could not create the metadata because a MULIT_TEXT without associated option sets was found', }; - static buildtetFeatureType(featureType: 'POINT' | 'POLYGON') { + static buildtetFeatureType(featureType: 'POINT' | 'POLYGON', section: Section) { const dataElement = new DataElement((o) => { + o.section = section; o.id = `FEATURETYPE_${featureType}`; o.name = featureType === 'POINT' ? i18n.t('Coordinate') : i18n.t('Area'); o.formName = o.name; @@ -206,8 +208,10 @@ export class DataElementFactory { async _buildBaseDataElement( cachedProgramTrackedEntityAttribute: CachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute: CachedTrackedEntityAttribute, + section?: Section, ) { const dataElement = new DataElement(); + dataElement.section = section; dataElement.type = cachedTrackedEntityAttribute.valueType; await this._setBaseProperties( dataElement, @@ -224,10 +228,12 @@ export class DataElementFactory { async _buildDateDataElement( cachedProgramTrackedEntityAttribute: CachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute: CachedTrackedEntityAttribute, + section?: Section, ) { const dateDataElement = new DateDataElement(); dateDataElement.type = dataElementTypes.DATE; dateDataElement.allowFutureDate = cachedProgramTrackedEntityAttribute.allowFutureDate; + dateDataElement.section = section; await this._setBaseProperties( dateDataElement, cachedProgramTrackedEntityAttribute, @@ -238,6 +244,7 @@ export class DataElementFactory { build( cachedProgramTrackedEntityAttribute: CachedProgramTrackedEntityAttribute, + section?: Section, ) { const cachedTrackedEntityAttribute = cachedProgramTrackedEntityAttribute.trackedEntityAttributeId && this.cachedTrackedEntityAttributes.get( @@ -253,7 +260,7 @@ export class DataElementFactory { } return cachedTrackedEntityAttribute.valueType === dataElementTypes.DATE ? - this._buildDateDataElement(cachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute) : - this._buildBaseDataElement(cachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute); + this._buildDateDataElement(cachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute, section) : + this._buildBaseDataElement(cachedProgramTrackedEntityAttribute, cachedTrackedEntityAttribute, section); } } diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js index 19230eace9..3358d5f648 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/enrollment/EnrollmentFactory.js @@ -78,7 +78,7 @@ export class EnrollmentFactory { }); } - _buildTetFeatureTypeField(trackedEntityTypeId: ?string) { + _buildTetFeatureTypeField(trackedEntityTypeId: ?string, section: Section) { const teType = trackedEntityTypeId && this.cachedTrackedEntityTypes.get(trackedEntityTypeId); if (!teType) { return null; @@ -90,23 +90,24 @@ export class EnrollmentFactory { } // $FlowFixMe - return DataElementFactory.buildtetFeatureType(featureType); + return DataElementFactory.buildtetFeatureType(featureType, section); } async _buildTetFeatureTypeSection( cachedProgramTrackedEntityTypeId: string, ) { - const featureTypeField = this._buildTetFeatureTypeField(cachedProgramTrackedEntityTypeId); const trackedEntityType = this.cachedTrackedEntityTypes.get(cachedProgramTrackedEntityTypeId); - if (!featureTypeField) { - return null; - } - const section = new Section((o) => { o.id = cachedProgramTrackedEntityTypeId; o.name = trackedEntityType?.displayName || ''; + o.group = Section.groups.ENROLLMENT; }); + const featureTypeField = this._buildTetFeatureTypeField(cachedProgramTrackedEntityTypeId, section); + + if (!featureTypeField) { + return null; + } featureTypeField && section.addElement(featureTypeField); return section; @@ -119,12 +120,13 @@ export class EnrollmentFactory { const section = new Section((o) => { o.id = Section.MAIN_SECTION_ID; o.name = i18n.t('Profile'); + o.group = Section.groups.ENROLLMENT; }); if (!cachedProgramTrackedEntityAttributes?.length) { return null; } if (cachedProgramTrackedEntityTypeId) { - const featureTypeField = this._buildTetFeatureTypeField(cachedProgramTrackedEntityTypeId); + const featureTypeField = this._buildTetFeatureTypeField(cachedProgramTrackedEntityTypeId, section); featureTypeField && section.addElement(featureTypeField); } @@ -166,7 +168,7 @@ export class EnrollmentFactory { element && section.addElement(element); } else { - const element = await this.dataElementFactory.build(trackedEntityAttribute); + const element = await this.dataElementFactory.build(trackedEntityAttribute, section); element && section.addElement(element); } }); @@ -185,6 +187,7 @@ export class EnrollmentFactory { const section = new Section((o) => { o.id = cachedSectionCustomId; o.name = cachedSectionCustomLabel; + o.group = Section.groups.ENROLLMENT; }); await this._buildElementsForSection(cachedProgramTrackedEntityAttributes, section); @@ -200,6 +203,7 @@ export class EnrollmentFactory { let section = new Section((o) => { o.id = Section.MAIN_SECTION_ID; + o.group = Section.groups.ENROLLMENT; }); section.showContainer = false; @@ -353,6 +357,7 @@ export class EnrollmentFactory { const foundation = new RenderFoundation(); const section = new Section((oSection) => { oSection.id = Section.MAIN_SECTION_ID; + oSection.group = Section.groups.ENROLLMENT; }); Array.from( searchGroupFoundation diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.js index b76d755531..28ff91fe67 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.js @@ -175,6 +175,7 @@ export class ProgramFactory { program.icon = buildIcon(cachedProgram.style); program.displayFrontPageList = cachedProgram.displayFrontPageList; program.onlyEnrollOnce = cachedProgram.onlyEnrollOnce; + program.useFirstStageDuringRegistration = cachedProgram.useFirstStageDuringRegistration; return program; } diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js index 389b6dcc4c..6bd83c7f08 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/DataElementFactory.js @@ -9,7 +9,7 @@ import type { CachedProgramStageDataElement, CachedOptionSet, } from '../../../../storageControllers/cache.types'; -import { DataElement, DateDataElement, dataElementTypes } from '../../../../metaData'; +import { DataElement, DateDataElement, dataElementTypes, Section } from '../../../../metaData'; import { buildIcon } from '../../../common/helpers'; import { OptionSetFactory } from '../../../common/factory'; import { isNotValidOptionSet } from '../../../../utils/isNotValidOptionSet'; @@ -99,8 +99,10 @@ export class DataElementFactory { cachedProgramStageDataElement: CachedProgramStageDataElement, cachedDataElement: CachedDataElement, dataElementType: $Keys, + section: ?Section, ) { const dataElement = new DataElement(); + dataElement.section = section; dataElement.type = dataElementType; await this._setBaseProperties(dataElement, cachedProgramStageDataElement, cachedDataElement); if (isNotValidOptionSet(dataElement.type, dataElement.optionSet)) { @@ -123,6 +125,7 @@ export class DataElementFactory { async build( cachedProgramStageDataElement: CachedProgramStageDataElement, + section: ?Section, ): Promise { const cachedDataElement = await getUserStorageController().get(userStores.DATA_ELEMENTS, cachedProgramStageDataElement.dataElementId); @@ -139,6 +142,6 @@ export class DataElementFactory { return dataElementType === dataElementTypes.DATE ? this._buildDateDataElement(cachedProgramStageDataElement, cachedDataElement) : - this._buildBaseDataElement(cachedProgramStageDataElement, cachedDataElement, dataElementType); + this._buildBaseDataElement(cachedProgramStageDataElement, cachedDataElement, dataElementType, section); } } diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js index adc65c2f5a..83bc11b8ff 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.js @@ -65,7 +65,7 @@ export class ProgramStageFactory { { sectionDataElement })); return; } - const element = await this.dataElementFactory.build(cachedProgramStageDataElement); + const element = await this.dataElementFactory.build(cachedProgramStageDataElement, section); element && section.addElement(element); }); } @@ -81,7 +81,7 @@ export class ProgramStageFactory { if (cachedProgramStageDataElements) { // $FlowFixMe await cachedProgramStageDataElements.asyncForEach((async (cachedProgramStageDataElement) => { - const element = await this.dataElementFactory.build(cachedProgramStageDataElement); + const element = await this.dataElementFactory.build(cachedProgramStageDataElement, section); element && section.addElement(element); })); } @@ -128,6 +128,7 @@ export class ProgramStageFactory { _stage.generatedByEnrollmentDate = !!cachedProgramStage.generatedByEnrollmentDate; _stage.reportDateToUse = cachedProgramStage.reportDateToUse; _stage.minDaysFromStart = cachedProgramStage.minDaysFromStart; + _stage.repeatable = cachedProgramStage.repeatable; _stage.stageForm = new RenderFoundation((_form) => { _form.id = cachedProgramStage.id; _form.name = cachedProgramStage.displayName; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js index 1e04d03ec7..adebf39a14 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.js @@ -80,7 +80,7 @@ const convert = (() => { }; })(); -const fieldsParam = 'id,displayName,displayShortName,description,programType,style,displayFrontPageList,onlyEnrollOnce,' + +const fieldsParam = 'id,displayName,displayShortName,description,programType,style,displayFrontPageList,useFirstStageDuringRegistration,onlyEnrollOnce,' + 'minAttributesRequiredToSearch,enrollmentDateLabel,incidentDateLabel,' + 'featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,displayIncidentDate,' + 'dataEntryForm[id,htmlCode],' + diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js index 32c02464d8..70ac391972 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.js @@ -140,6 +140,7 @@ type apiProgram = { featureType?: ?string, selectEnrollmentDatesInFuture: boolean, displayFrontPageList: boolean, + useFirstStageDuringRegistration: boolean, selectIncidentDatesInFuture: boolean, displayIncidentDate: boolean, dataEntryForm?: ?apiDataEntryForm, 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, }, diff --git a/src/core_modules/capture-core/rules/getApplicableRuleEffects.js b/src/core_modules/capture-core/rules/getApplicableRuleEffects.js index 57ffddac38..2d261aef78 100644 --- a/src/core_modules/capture-core/rules/getApplicableRuleEffects.js +++ b/src/core_modules/capture-core/rules/getApplicableRuleEffects.js @@ -74,6 +74,7 @@ export const getApplicableRuleEffectsForTrackerProgram = ({ otherEvents, attributeValues, enrollmentData, + formFoundation, }: GetApplicableRuleEffectsForTrackerProgramInput, flattenedResult: boolean = false, ) => { @@ -82,6 +83,7 @@ flattenedResult: boolean = false, program.programRules, stage?.programRules, ); + const foundationForPostProcessing = formFoundation || (stage ? stage.stageForm : program.enrollment.enrollmentForm); if (!programRules.length) { return []; } @@ -103,7 +105,7 @@ flattenedResult: boolean = false, programRules, programRuleVariables, trackedEntityAttributes: getTrackedEntityAttributesForRulesExecution(program.attributes), - foundationForPostProcessing: stage ? stage.stageForm : program.enrollment.enrollmentForm, + foundationForPostProcessing, }); return flattenedResult ? effects : buildEffectsHierarchy(effects); diff --git a/src/core_modules/capture-core/rules/rules.types.js b/src/core_modules/capture-core/rules/rules.types.js index 606ae8336f..0249ee664c 100644 --- a/src/core_modules/capture-core/rules/rules.types.js +++ b/src/core_modules/capture-core/rules/rules.types.js @@ -19,6 +19,7 @@ export type GetApplicableRuleEffectsForTrackerProgramInput = {| otherEvents?: EventsData, attributeValues?: TEIValues, enrollmentData?: Enrollment, + formFoundation?: RenderFoundation, |}; export type GetApplicableRuleEffectsForEventProgramInput = {| diff --git a/src/core_modules/capture-core/storageControllers/cache.types.js b/src/core_modules/capture-core/storageControllers/cache.types.js index cfcaa75c92..df2cffbb41 100644 --- a/src/core_modules/capture-core/storageControllers/cache.types.js +++ b/src/core_modules/capture-core/storageControllers/cache.types.js @@ -119,6 +119,7 @@ export type CachedProgramStage = { generatedByEnrollmentDate?: ?boolean, hideDueDate?: ?boolean, reportDateToUse: string, + repeatable: boolean, minDaysFromStart: number, style?: ?CachedStyle, }; @@ -204,6 +205,7 @@ export type CachedProgram = { selectIncidentDatesInFuture: boolean, displayIncidentDate: boolean, onlyEnrollOnce: boolean, + useFirstStageDuringRegistration: boolean, }; export type CachedProgramStageDataElementsAsObject = {