Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DHIS2-15906] Add form features for relationship #3432

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## [100.41.2](https://github.com/dhis2/capture-app/compare/v100.41.1...v100.41.2) (2023-10-12)


### Bug Fixes

* [DHIS2-15827] remove effects of 'Prevent adding new events to stage' action from the form ([#3418](https://github.com/dhis2/capture-app/issues/3418)) ([f17d087](https://github.com/dhis2/capture-app/commit/f17d087f368cf4d6b26923fd5cf4fb15d7795ba2))

## [100.41.1](https://github.com/dhis2/capture-app/compare/v100.41.0...v100.41.1) (2023-10-09)


### Bug Fixes

* [DHIS2-15734] assign effect dynamic formId object key ([#3422](https://github.com/dhis2/capture-app/issues/3422)) ([85f242b](https://github.com/dhis2/capture-app/commit/85f242b2b0c6092b46e92f087e21e102a2e1e19d))

# [100.41.0](https://github.com/dhis2/capture-app/compare/v100.40.1...v100.41.0) (2023-10-03)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ Feature: Hidden program stage
Scenario: The user cannot add an event in a hidden program stage
Given you add an enrollment event that will result in a rule effect to hide a program stage
Then the New Postpartum care visit event button is disabled in the stages and events widget
And and an error is show in the Postpartum care visit stage
And the Postpartum care visit button is disabled in the enrollmentEventNew page
12 changes: 0 additions & 12 deletions cypress/integration/EnrollmentPage/HiddenProgramStage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,6 @@ Then('the New Postpartum care visit event button is disabled in the stages and e
.should('be.disabled');
});

Then('and an error is show in the Postpartum care visit stage', () => {
cy.visit(
'/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA&stageId=bbKtnxRZKEP',
);
cy.contains('[data-test="dhis2-uicore-button"]', 'Complete')
.should('be.disabled');
cy.contains('[data-test="dhis2-uicore-button"]', 'Save without completing')
.should('be.disabled');
cy.contains('[data-test="dhis2-uicore-noticebox-content"]', 'You can\'t add any more Postpartum care visit events')
.should('exist');
});

Then('the Postpartum care visit button is disabled in the enrollmentEventNew page', () => {
cy.visit(
'/#/enrollmentEventNew?enrollmentId=fmhIsWXVDmS&orgUnitId=s7SLtx8wmRA&programId=WSGAb5XwJ3Y&teiId=uW8Y7AIcRKA',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions docs/user/using-the-capture-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "capture-app",
"homepage": ".",
"version": "100.41.0",
"version": "100.41.2",
"cacheVersion": "6",
"serverVersion": "38",
"license": "BSD-3-Clause",
Expand All @@ -10,7 +10,7 @@
"packages/rules-engine"
],
"dependencies": {
"@dhis2/rules-engine-javascript": "100.41.0",
"@dhis2/rules-engine-javascript": "100.41.2",
"@dhis2/app-runtime": "^3.9.3",
"@dhis2/d2-i18n": "^1.1.0",
"@dhis2/d2-icons": "^1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/rules-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dhis2/rules-engine-javascript",
"version": "100.41.0",
"version": "100.41.2",
"license": "BSD-3-Clause",
"main": "./build/cjs/index.js",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type { OwnProps } from './EnrollmentRegistrationEntry.types';
import { useLifecycle } from './hooks';
import { useRulesEngineOrgUnit } from '../../../hooks';
import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges';
import {
useBuildEnrollmentPayload,
} from './hooks/useBuildEnrollmentPayload';

export const EnrollmentRegistrationEntry: ComponentType<OwnProps> = ({
selectedScopeId,
Expand All @@ -26,7 +29,14 @@ export const EnrollmentRegistrationEntry: ComponentType<OwnProps> = ({
formId,
enrollmentMetadata,
formFoundation,
} = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit);
} = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit, teiId, selectedScopeId);
const { buildTeiWithEnrollment } = useBuildEnrollmentPayload({
programId: selectedScopeId,
dataEntryId: id,
orgUnitId,
teiId,
trackedEntityTypeId: enrollmentMetadata?.trackedEntityType?.id,
});

const isUserInteractionInProgress: boolean = useSelector(
state =>
Expand All @@ -40,10 +50,16 @@ export const EnrollmentRegistrationEntry: ComponentType<OwnProps> = ({
const isSavingInProgress = useSelector(({ possibleDuplicates, newPage }) =>
possibleDuplicates.isLoading || possibleDuplicates.isUpdating || !!newPage.uid);


if (error) {
return error.errorComponent;
}

const onSaveWithEnrollment = () => {
const teiWithEnrollment = buildTeiWithEnrollment();
onSave(teiWithEnrollment);
};

return (
<EnrollmentRegistrationEntryComponent
{...passOnProps}
Expand All @@ -61,7 +77,7 @@ export const EnrollmentRegistrationEntry: ComponentType<OwnProps> = ({
orgUnit={orgUnit}
isUserInteractionInProgress={isUserInteractionInProgress}
isSavingInProgress={isSavingInProgress}
onSave={() => onSave(formFoundation, firstStageMetaData)}
onSave={onSaveWithEnrollment}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMess
import type { InputAttribute } from './hooks/useFormValues';
import { RenderFoundation, ProgramStage } from '../../../metaData';

export type EnrollmentPayload = {|
trackedEntity: string,
trackedEntityType: string,
orgUnit: string,
geometry: any,
enrollments: [
{|
occurredAt: string,
orgUnit: string,
program: string,
status: string,
enrolledAt: string,
events: Array<{
orgUnit: string,
}>,
attributes: Array<{
attribute: string,
value: any,
}>,
|}
]
|}

export type OwnProps = $ReadOnly<{|
id: string,
orgUnitId: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// @flow
import { useSelector } from 'react-redux';
import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey';
import {
getTrackerProgramThrowIfNotFound,
Section,
} from '../../../../metaData';
import type { RenderFoundation } from '../../../../metaData';
import { convertClientToServer, convertFormToClient } from '../../../../converters';
import {
convertDataEntryValuesToClientValues,
} from '../../../DataEntry/common/convertDataEntryValuesToClientValues';
import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string';
import { generateUID } from '../../../../utils/uid/generateUID';
import {
useBuildFirstStageRegistration,
} from './useBuildFirstStageRegistration';
import {
useMetadataForRegistrationForm,
} from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm';
import {
useMergeFormFoundationsIfApplicable,
} from './useMergeFormFoundationsIfApplicable';
import {
deriveAutoGenerateEvents,
deriveFirstStageDuringRegistrationEvent,
} from '../../../Pages/New/RegistrationDataEntry/helpers';
import { FEATURETYPE } from '../../../../constants';
import type { EnrollmentPayload } from '../EnrollmentRegistrationEntry.types';

type DataEntryReduxConverterProps = {
programId: string;
dataEntryId: string;
itemId?: string;
orgUnitId: string;
teiId: ?string;
trackedEntityTypeId: string;
};

function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) {
const clientValues = formFoundation.convertValues(formValues, convertFormToClient);
return clientValues;
}

function getServerValuesForMainValues(
values: Object,
meta: Object,
formFoundation: RenderFoundation,
) {
const clientValues = convertDataEntryValuesToClientValues(
values,
meta,
formFoundation,
) || {};

// potientally run this through a server to client converter for enrollment, the same way as for event
const serverValues = Object
.keys(clientValues)
.reduce((acc, key) => {
const value = clientValues[key];
const type = meta[key].type;
acc[key] = convertClientToServer(value, type);
return acc;
}, {});

return serverValues;
}

function getPossibleTetFeatureTypeKey(serverValues: Object) {
return Object
.keys(serverValues)
.find(key => key.startsWith('FEATURETYPE_'));
}

function buildGeometryProp(key: string, serverValues: Object) {
if (!serverValues[key]) {
return undefined;
}
const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase());
return {
type,
coordinates: serverValues[key],
};
}

const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey);

const deriveAttributesFromFormValues = (formValues = {}) =>
Object.keys(formValues)
.filter(key => !geometryType(key))
.map<{ attribute: string, value: ?any }>(key => ({ attribute: key, value: formValues[key] }));

export const useBuildEnrollmentPayload = ({
programId,
dataEntryId,
itemId = 'newEnrollment',
orgUnitId,
teiId,
trackedEntityTypeId,
}: DataEntryReduxConverterProps) => {
const dataEntryKey = getDataEntryKey(dataEntryId, itemId);
const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]);
const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]);
const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]);
const { formFoundation: scopeFormFoundation } = useMetadataForRegistrationForm({ selectedScopeId: programId });
const { firstStageMetaData } = useBuildFirstStageRegistration(programId);
const { formFoundation } = useMergeFormFoundationsIfApplicable(scopeFormFoundation, firstStageMetaData);

const buildTeiWithEnrollment = (): EnrollmentPayload => {
if (!formFoundation) throw Error('form foundation object not found');
const firstStage = firstStageMetaData && firstStageMetaData.stage;
const clientValues = getClientValuesForFormData(formValues, formFoundation);
const serverValuesForFormValues = formFoundation.convertAndGroupBySection(clientValues, convertClientToServer);
const serverValuesForMainValues = getServerValuesForMainValues(
dataEntryFieldValues,
dataEntryFieldsMeta,
formFoundation,
);
const { enrolledAt, occurredAt } = serverValuesForMainValues;

const { stages } = getTrackerProgramThrowIfNotFound(programId);

const attributeCategoryOptionsId = 'attributeCategoryOptions';
const attributeCategoryOptions = Object.keys(serverValuesForMainValues)
.filter(key => key.startsWith(attributeCategoryOptionsId))
.reduce((acc, key) => {
const categoryId = key.split('-')[1];
acc[categoryId] = serverValuesForMainValues[key];
return acc;
}, {});

const formServerValues = serverValuesForFormValues[Section.groups.ENROLLMENT];
const currentEventValues = serverValuesForFormValues[Section.groups.EVENT];


const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({
firstStageMetadata: firstStage,
programId,
orgUnitId,
currentEventValues,
fieldsValue: dataEntryFieldValues,
attributeCategoryOptions,
});

const autoGenerateEvents = deriveAutoGenerateEvents({
firstStageMetadata: firstStage,
stages,
enrolledAt,
occurredAt,
programId,
orgUnitId,
attributeCategoryOptions,
});

const allEventsToBeCreated = firstStageDuringRegistrationEvent
? [firstStageDuringRegistrationEvent, ...autoGenerateEvents]
: autoGenerateEvents;

const enrollment = {
program: programId,
status: 'ACTIVE',
orgUnit: orgUnitId,
occurredAt,
enrolledAt,
attributes: deriveAttributesFromFormValues(formServerValues),
events: allEventsToBeCreated,
};

const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues);
let geometry;
if (tetFeatureTypeKey) {
geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues);
delete serverValuesForFormValues[tetFeatureTypeKey];
}

return {
trackedEntity: teiId || generateUID(),
orgUnit: orgUnitId,
trackedEntityType: trackedEntityTypeId,
geometry,
enrollments: [enrollment],
};
};

return {
buildTeiWithEnrollment,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useEffect, useRef } from 'react';
import type { OrgUnit } from '@dhis2/rules-engine-javascript';
import { startNewEnrollmentDataEntryInitialisation } from '../EnrollmentRegistrationEntry.actions';
import { scopeTypes, getProgramThrowIfNotFound } from '../../../../metaData';
import { useLocationQuery } from '../../../../utils/routing';
import { useScopeInfo } from '../../../../hooks/useScopeInfo';
import { useFormValues } from './index';
import type { InputAttribute } from './useFormValues';
Expand All @@ -19,8 +18,8 @@ export const useLifecycle = (
trackedEntityInstanceAttributes?: Array<InputAttribute>,
orgUnit: ?OrgUnit,
teiId: ?string,
programId: string,
) => {
const { programId } = useLocationQuery();
const dataEntryReadyRef = useRef(false);
const dispatch = useDispatch();
const program = programId && getProgramThrowIfNotFound(programId);
Expand Down
Loading
Loading