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
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,189 @@
// @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 => {
// $FlowFixMe - Business logic dictates that formFoundation is not null in this callback
if (!formFoundation) return null;
eirikhaugstulen marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TeiRegistrationEntryComponent } from './TeiRegistrationEntry.component'
import { useFormValuesFromSearchTerms } from './hooks/useFormValuesFromSearchTerms';
import { dataEntryHasChanges } from '../../DataEntry/common/dataEntryHasChanges';
import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm';
import { useBuildTeiPayload } from './hooks/useBuildTeiPayload';

const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -42,13 +43,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) =
};


export const TeiRegistrationEntry: ComponentType<OwnProps> = ({ selectedScopeId, id, orgUnitId, ...rest }) => {
export const TeiRegistrationEntry: ComponentType<OwnProps> = ({ selectedScopeId, id, orgUnitId, onSave, ...rest }) => {
const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id, orgUnitId);
const ready = useSelector(({ dataEntries }) => (!!dataEntries[id]));
const dataEntry = useSelector(({ dataEntries }) => (dataEntries[id]));
const {
registrationMetaData: teiRegistrationMetadata,
} = useMetadataForRegistrationForm({ selectedScopeId });
const { buildTeiWithoutEnrollment } = useBuildTeiPayload({
trackedEntityTypeId: selectedScopeId,
dataEntryId: id,
orgUnitId,
});

const dataEntryKey = useMemo(() => {
if (dataEntry) {
Expand All @@ -66,6 +72,11 @@ export const TeiRegistrationEntry: ComponentType<OwnProps> = ({ selectedScopeId,
return null;
}

const onSaveWithoutEnrollment = () => {
const teiPayload = buildTeiWithoutEnrollment();
onSave(teiPayload);
};

return (
<TeiRegistrationEntryComponent
id={id}
Expand All @@ -75,6 +86,7 @@ export const TeiRegistrationEntry: ComponentType<OwnProps> = ({ selectedScopeId,
ready={ready && !!teiRegistrationMetadata}
trackedEntityName={trackedEntityName}
isUserInteractionInProgress={isUserInteractionInProgress}
onSave={onSaveWithoutEnrollment}
{...rest}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
import type { Node } from 'react';
import type { RegistrationFormMetadata } from '../common/TEIAndEnrollment/useMetadataForRegistrationForm/types';
import type { RenderCustomCardActions } from '../../CardList';
import type { SaveForDuplicateCheck } from '../common/TEIAndEnrollment/DuplicateCheckOnSave';
import type { ExistingUniqueValueDialogActionsComponent } from '../withErrorMessagePostProcessor';
import type {
TeiPayload,
} from '../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types';

export type OwnProps = $ReadOnly<{|
id: string,
orgUnitId: string,
selectedScopeId: string,
saveButtonText: string,
fieldOptions?: Object,
onSave: SaveForDuplicateCheck,
onSave: (TeiPayload) => void,
duplicatesReviewPageSize: number,
isSavingInProgress?: boolean,
renderDuplicatesCardActions?: RenderCustomCardActions,
renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node,
renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node,
ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent,
|}>;

Expand All @@ -39,9 +41,9 @@ type PropsAddedInHOC = {|
|};
type PropsRemovedInHOC = {|
renderDuplicatesCardActions?: RenderCustomCardActions,
renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node,
renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node,
duplicatesReviewPageSize: number,
onSave: SaveForDuplicateCheck,
onSave: (TeiPayload) => void,
|};

export type PlainProps = {|
Expand Down
Loading