From 72ac5356c6e6bba9378be33e6c9415a4deee158e Mon Sep 17 00:00:00 2001 From: eirikhaugstulen Date: Mon, 14 Aug 2023 11:17:10 +0200 Subject: [PATCH] feat: create new tei from relationship --- i18n/en.pot | 10 +- .../EnrollmentRegistrationEntry.container.js | 9 +- .../EnrollmentRegistrationEntry.types.js | 2 +- .../hooks/useLifecycle.js | 3 +- .../TeiRegistrationEntry.container.js | 1 + .../RegistrationDataEntry.component.js | 2 + .../DataEntryEnrollment.component.js | 5 +- .../DataEntryEnrollment.component.js | 2 +- .../RegisterTeiDataEntry.component.js | 1 + .../DataEntryTrackedEntityInstance.js | 1 - .../dataEntryTrackedEntityInstance.types.js | 1 + .../RegisterTei/RegisterTei.component.js | 17 +- .../RegisterTei/RegisterTei.container.js | 24 ++- .../RegisterTei/RegisterTei.types.js | 27 ++-- .../TeiSearch/TeiSearch.component.js | 138 ++++++++-------- ...ackedEntityRelationshipsWrapper.actions.js | 5 +- ...kedEntityRelationshipsWrapper.component.js | 41 +++-- ...TrackedEntityRelationshipsWrapper.epics.js | 23 +-- .../hooks/useDataEntryReduxConverter.js | 148 ++++++++++++++++++ .../NewTrackedEntityRelationship.component.js | 68 ++++++-- .../NewTrackedEntityRelationship.types.js | 15 +- .../hooks/useAddRelationship.js | 30 +++- .../WidgetTrackedEntityRelationship.types.js | 12 +- .../capture-core/utils/uid/generateUID.js | 18 +++ 24 files changed, 420 insertions(+), 183 deletions(-) create mode 100644 src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js create mode 100644 src/core_modules/capture-core/utils/uid/generateUID.js diff --git a/i18n/en.pot b/i18n/en.pot index f6ff4d53d8..56c9d65eb3 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-07-28T10:44:57.813Z\n" -"PO-Revision-Date: 2023-07-28T10:44:57.813Z\n" +"POT-Creation-Date: 2023-08-14T09:17:11.587Z\n" +"PO-Revision-Date: 2023-08-14T09:17:11.587Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -1365,6 +1365,12 @@ msgstr "New Relationship" msgid "Link to an existing {{tetName}}" msgstr "Link to an existing {{tetName}}" +msgid "An error occurred while adding the relationship" +msgstr "An error occurred while adding the relationship" + +msgid "Relationship added" +msgstr "Relationship added" + msgid "Something went wrong while loading relationships. Please try again later." msgstr "Something went wrong while loading relationships. Please try again later." 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 703f82fe53..18268895d6 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 @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux'; import { EnrollmentRegistrationEntryComponent } from './EnrollmentRegistrationEntry.component'; import type { OwnProps } from './EnrollmentRegistrationEntry.types'; 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'; @@ -15,12 +14,12 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ id, saveButtonText, trackedEntityInstanceAttributes, - cachedOrgUnitId, + orgUnitId, + teiId, ...passOnProps }) => { - const orgUnitId = useCurrentOrgUnitInfo().id; - const { orgUnit, error } = useRulesEngineOrgUnit(cachedOrgUnitId ?? orgUnitId); - const { teiId, ready, skipDuplicateCheck } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit); + const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); + const { ready, skipDuplicateCheck } = useLifecycle(selectedScopeId, id, trackedEntityInstanceAttributes, orgUnit, teiId); const { formId, registrationMetaData: enrollmentMetadata, 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 e844a094e8..7242d3963d 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 @@ -10,6 +10,7 @@ import { RenderFoundation } from '../../../metaData'; export type OwnProps = $ReadOnly<{| id: string, + orgUnitId: string, selectedScopeId: string, fieldOptions?: Object, onSave: SaveForDuplicateCheck, @@ -21,7 +22,6 @@ export type OwnProps = $ReadOnly<{| skipDuplicateCheck?: ?boolean, trackedEntityInstanceAttributes?: Array, saveButtonText: (trackedEntityName: string) => string, - cachedOrgUnitId?: string, |}>; type ContainerProps = {| 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..fa71aaf6df 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 @@ -16,8 +16,9 @@ export const useLifecycle = ( dataEntryId: string, trackedEntityInstanceAttributes?: Array, orgUnit: ?OrgUnit, + teiId: ?string, ) => { - const { teiId, programId } = useLocationQuery(); + const { programId } = useLocationQuery(); const dataEntryReadyRef = useRef(false); const dispatch = useDispatch(); const program = programId && getProgramThrowIfNotFound(programId); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js index ffce8d6311..6f1efdc57e 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js @@ -69,6 +69,7 @@ export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, return ( onSaveWithEnrollment(formFoundation)} saveButtonText={(trackedEntityTypeNameLC: string) => i18n.t('Save {{trackedEntityTypeName}}', { diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js index 0de7c46415..19d78a8c31 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -6,6 +6,7 @@ import { DATA_ENTRY_ID } from '../../registerTei.const'; import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../DataEntries'; import type { Props } from './dataEntryEnrollment.types'; +import { useLocationQuery } from '../../../../../../utils/routing'; const NewEnrollmentRelationshipPlain = ({ @@ -17,13 +18,15 @@ const NewEnrollmentRelationshipPlain = renderDuplicatesCardActions, ExistingUniqueValueDialogActions, }: Props) => { + const { orgUnitId, teiId } = useLocationQuery(); const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - return ( i18n.t('Save new {{trackedEntityTypeName}} and link', { trackedEntityTypeName, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js index 1ef1030539..2dbf0c7a20 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -24,7 +24,7 @@ const NewEnrollmentRelationshipPlain = i18n.t('Save new {{trackedEntityTypeName}} and link', { trackedEntityTypeName, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js index 55f8c295aa..4b0ad71c40 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -15,6 +15,7 @@ export class RegisterTeiDataEntryComponent extends React.Component { if (!showDataEntry) { return null; } + if (programId) { return ( ({ @@ -57,15 +57,14 @@ const DialogButtons = ({ onCancel, onSave, trackedEntityName }) => ( const RegisterTeiPlain = ({ dataEntryId, - itemId, onLink, onSave, onGetUnsavedAttributeValues, trackedEntityName, trackedEntityTypeId, - selectedProgramId, + selectedScopeId, classes, -}: Props) => { +}: ComponentProps) => { const { resultsPageSize } = useContext(ResultsPageSizeContext); const renderDuplicatesCardActions = useCallback(({ item }) => ( @@ -94,10 +93,6 @@ const RegisterTeiPlain = ({ ), [onLink]); - const handleSave = useCallback(() => { - onSave(itemId, dataEntryId); - }, [onSave, itemId, dataEntryId]); - return (
@@ -106,7 +101,7 @@ const RegisterTeiPlain = ({ /> } @@ -126,7 +121,7 @@ const RegisterTeiPlain = ({ ); }; -export const RegisterTeiComponent: ComponentType<$Diff> = +export const RegisterTeiComponent: ComponentType<$Diff> = compose( withErrorMessageHandler(), withStyles(getStyles), diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js index c71313710f..5a49f7d14a 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js @@ -2,8 +2,9 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { RegisterTeiComponent } from './RegisterTei.component'; -import type { OwnProps } from './RegisterTei.types'; +import type { ContainerProps } from './RegisterTei.types'; import { useScopeInfo } from '../../../../../hooks/useScopeInfo'; +import { useDataEntryReduxConverter } from '../TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter'; export const RegisterTei = ({ onLink, @@ -11,22 +12,31 @@ export const RegisterTei = ({ onGetUnsavedAttributeValues, trackedEntityTypeId, suggestedProgramId, -}: OwnProps) => { +}: ContainerProps) => { const dataEntryId = 'relationship'; const itemId = useSelector(({ dataEntries }) => dataEntries[dataEntryId]?.itemId); const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); - const newRelationshipProgramId = suggestedProgramId || trackedEntityTypeId; - const { trackedEntityName } = useScopeInfo(newRelationshipProgramId); + const selectedScopeId = suggestedProgramId || trackedEntityTypeId; + const { trackedEntityName } = useScopeInfo(selectedScopeId); + const { buildTeiPayload } = useDataEntryReduxConverter({ + dataEntryId, + itemId, + trackedEntityTypeId, + }); + + const onCreateNewTei = () => { + const teiPayload = buildTeiPayload(); + onSave(teiPayload); + }; return ( diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js index 957ca0637f..022eeba97a 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js @@ -1,17 +1,22 @@ // @flow -type PropsFromRedux = {| - dataEntryId: string, - itemId: string, - trackedEntityName: ?string, - error: string, -|}; - -export type OwnProps = {| +export type SharedProps = {| onLink: (teiId: string, values: Object) => void, onGetUnsavedAttributeValues?: ?Function, - onSave: (itemId: string, dataEntryId: string) => void, - suggestedProgramId: string, trackedEntityTypeId: string, |}; -export type Props = {|...PropsFromRedux, ...OwnProps, ...CssClasses |} +export type ContainerProps = {| + suggestedProgramId: string, + onSave: (teiPayload: Object) => void, + ...SharedProps, +|}; + +export type ComponentProps = {| + selectedScopeId: string, + error: string, + dataEntryId: string, + trackedEntityName: ?string, + onSave: () => void, + ...SharedProps, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js index e15c1f3771..b4457973b6 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.js @@ -1,8 +1,7 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useCallback, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import withStyles from '@material-ui/core/styles/withStyles'; -import { SearchGroup } from '../../../../../metaData'; import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container'; import { SearchProgramSelector } from './SearchProgramSelector/SearchProgramSelector.container'; @@ -25,114 +24,106 @@ const getStyles = (theme: Theme) => ({ }, }); -type State = { - programSectionOpen: boolean, -} - -class TeiSearchPlain extends React.Component { - constructor(props) { - super(props); - this.state = { programSectionOpen: true }; - } +const TeiSearchPlain = (props) => { + const [programSectionOpen, setProgramSectionOpen] = useState(true); + const getFormId = useCallback((searchGroupId) => { + const contextId = props.selectedProgramId || props.selectedTrackedEntityTypeId || ''; + return `${props.id}-${contextId}-${searchGroupId}`; + }, [props.selectedProgramId, props.selectedTrackedEntityTypeId, props.id]); - getFormId = (searchGroupId: string) => { - const contextId = this.props.selectedProgramId || this.props.selectedTrackedEntityTypeId || ''; - return `${this.props.id}-${contextId}-${searchGroupId}`; - } - - handleSearch = (formId: string, searchGroupId: string) => { - const { id } = this.props; - this.props.onSearch(formId, searchGroupId, id); - } + const handleSearch = (formId, searchGroupId) => { + props.onSearch(formId, searchGroupId, props.id); + }; - handleSearchResultsChangePage = (pageNumber: number) => { - this.props.onSearchResultsChangePage(this.props.id, pageNumber); - } + const handleSearchResultsChangePage = (pageNumber) => { + props.onSearchResultsChangePage(props.id, pageNumber); + }; - handleNewSearch = () => { - this.props.onNewSearch(this.props.id); - } + const handleNewSearch = () => { + props.onNewSearch(props.id); + }; - handleEditSearch = () => { - this.props.onEditSearch(this.props.id); - } + const handleEditSearch = () => { + props.onEditSearch(props.id); + }; - handleSearchValidationFailed = (...args) => { - const { id } = this.props; - this.props.onSearchValidationFailed(...args, id); - } + const handleSearchValidationFailed = (...args) => { + props.onSearchValidationFailed(...args, props.id); + }; - renderSearchForms = (searchGroups: Array) => ( -
- {this.renderProgramSection()} - {this.renderSearchGroups(searchGroups)} + const renderSearchForms = searchGroups => ( +
+ {renderProgramSection()} + {renderSearchGroups(searchGroups)}
); - renderProgramSection = () => { - const isCollapsed = !this.state.programSectionOpen; + const renderProgramSection = () => { + const isCollapsed = !programSectionOpen; return (
{ this.setState({ programSectionOpen: !!isCollapsed }); }} + onChangeCollapseState={() => { setProgramSectionOpen(!isCollapsed); }} title={i18n.t('Program')} /> } >
); - } + }; - onChangeSectionCollapseState = (id) => { - if (this.props.openSearchGroupSection === id) { - this.props.onSetOpenSearchGroupSection(this.props.id, null); + const onChangeSectionCollapseState = (id) => { + if (props.openSearchGroupSection === id) { + props.onSetOpenSearchGroupSection(props.id, null); return; } - this.props.onSetOpenSearchGroupSection(this.props.id, id); - } + props.onSetOpenSearchGroupSection(props.id, id); + }; - renderSearchGroups = (searchGroups: Array) => searchGroups.map((sg, i) => { + const renderSearchGroups = searchGroups => searchGroups.map((sg, i) => { const searchGroupId = i.toString(); - const formId = this.getFormId(searchGroupId); + const formId = getFormId(searchGroupId); const header = sg.unique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : i18n.t('Search by attributes'); - const collapsed = this.props.openSearchGroupSection !== searchGroupId; + const collapsed = props.openSearchGroupSection !== searchGroupId; return (
{ this.onChangeSectionCollapseState(searchGroupId); }} + onChangeCollapseState={() => { onChangeSectionCollapseState(searchGroupId); }} isCollapsed={collapsed} title={header} - />} + /> + } >
); - }) - renderSearchResult = () => { + }); + + const renderSearchResult = () => { const { id, searchGroups, @@ -140,14 +131,15 @@ class TeiSearchPlain extends React.Component { selectedProgramId, selectedTrackedEntityTypeId, trackedEntityTypeName, - } = this.props; + } = props; + return ( { /> ); - } - - render() { - const searchGroups = this.props.searchGroups; + }; - if (this.props.showResults) { - return this.renderSearchResult(); - } + const searchGroups = props.searchGroups; - return searchGroups ? this.renderSearchForms(searchGroups) : (
); + if (props.showResults) { + return renderSearchResult(); } -} + + return searchGroups ? renderSearchForms(searchGroups) : (
); +}; export const TeiSearchComponent: ComponentType<$Diff> = withStyles(getStyles)(TeiSearchPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js index 7c67450eee..bc5e85c1f6 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.actions.js @@ -1,6 +1,5 @@ // @flow import { actionCreator } from '../../../../../actions/actions.utils'; -import type { OnSelectFindModeProps } from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export const actionTypes = { WIDGET_SELECT_FIND_MODE: 'WidgetSelectFindMode', @@ -10,5 +9,5 @@ export const batchActionTypes = { BATCH_OPEN_TEI_SEARCH_WIDGET: 'batchOpenTeiSearchWidget', }; -export const selectFindMode = ({ findMode, relationshipConstraint, orgUnitId }: OnSelectFindModeProps) => - actionCreator(actionTypes.WIDGET_SELECT_FIND_MODE)({ findMode, relationshipConstraint, orgUnitId }); +export const selectFindMode = ({ findMode, orgUnit, relationshipConstraint }: Object) => + actionCreator(actionTypes.WIDGET_SELECT_FIND_MODE)({ findMode, orgUnit, relationshipConstraint }); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js index d0abc5b278..cf00570c97 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.component.js @@ -13,12 +13,9 @@ import { TeiSearch } from '../TeiSearch/TeiSearch.container'; import { TeiRelationshipSearchResults, } from '../../../NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component'; -import type { - OnLinkToTrackedEntity, -} from '../../../../WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types'; import { ResultsPageSizeContext } from '../../../shared-contexts'; -import { RegisterTeiComponent } from '../RegisterTei/RegisterTei.component'; import { RegisterTei } from '../RegisterTei'; +import { useOrganisationUnit } from '../../../../../dataQueries'; export const TrackedEntityRelationshipsWrapper = ({ trackedEntityTypeId, @@ -32,9 +29,14 @@ export const TrackedEntityRelationshipsWrapper = ({ }: Props) => { const dispatch = useDispatch(); const { relationshipTypes, isError } = useTEIRelationshipsWidgetMetadata(); + const { orgUnit: initialOrgUnit } = useOrganisationUnit(orgUnitId, 'id,displayName,code'); - const onSelectFindMode = ({ findMode, relationshipConstraint, orgUnitId }: OnSelectFindModeProps) => { - dispatch(selectFindMode({ findMode, orgUnitId, relationshipConstraint })); + const onSelectFindMode = ({ findMode, relationshipConstraint }: OnSelectFindModeProps) => { + dispatch(selectFindMode({ + findMode, + orgUnit: initialOrgUnit, + relationshipConstraint, + })); }; if (isError) { @@ -64,30 +66,25 @@ export const TrackedEntityRelationshipsWrapper = ({ onSelectFindMode={onSelectFindMode} relationshipTypes={relationshipTypes} renderTrackedEntityRegistration={( - selectedTrackedEntityTypeId: string, - suggestedProgramId: string, + selectedTrackedEntityTypeId, + suggestedProgramId, + onLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch, ) => ( - {/* */} console.log('link')} - onSave={() => console.log('save')} + suggestedProgramId={suggestedProgramId} + onLink={onLinkToTrackedEntityFromSearch} + onSave={onLinkToTrackedEntityFromRegistration} onGetUnsavedAttributeValues={() => console.log('get unsaved')} trackedEntityTypeId={selectedTrackedEntityTypeId} /> )} renderTrackedEntitySearch={( - searchTrackedEntityTypeId: string, - searchProgramId: string, - onLinkToTrackedEntity: OnLinkToTrackedEntity, + searchTrackedEntityTypeId, + searchProgramId, + onLinkToTrackedEntityFromSearch, ) => ( ( )} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js index af2bedb9c6..488faf8e6f 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/TrackedEntityRelationshipsWrapper.epics.js @@ -35,16 +35,6 @@ function getTrackerProgram(suggestedProgramId: string) { return trackerProgram; } -function getOrgUnitId(suggestedOrgUnitId: string, trackerProgram: ?TrackerProgram) { - let orgUnitId; - if (trackerProgram) { - orgUnitId = trackerProgram.organisationUnits[suggestedOrgUnitId] ? suggestedOrgUnitId : null; - } else { - orgUnitId = suggestedOrgUnitId; - } - return orgUnitId; -} - export const openRelationshipTeiSearchWidgetEpic = (action$: InputObservable) => action$.pipe( @@ -71,13 +61,12 @@ export const openRelationshipTeiSearchWidgetEpic = }), ); -export const openRelationshipTeiRegisterWidgetEpic = (action$: InputObservable, store: ReduxStore) => +export const openRelationshipTeiRegisterWidgetEpic = (action$: InputObservable) => action$.pipe( ofType(actionTypes.WIDGET_SELECT_FIND_MODE), filter(action => action.payload.findMode && action.payload.findMode === findModes.TEI_REGISTER), switchMap((action) => { - const state = store.value; - const { relationshipConstraint, orgUnitId: suggestedOrgUnitId } = action.payload; + const { relationshipConstraint, orgUnit } = action.payload; const { programId } = relationshipConstraint; let trackerProgram: ?TrackerProgram; @@ -88,17 +77,11 @@ export const openRelationshipTeiRegisterWidgetEpic = (action$: InputObservable, return Promise.resolve(initializeRegisterTeiFailed(error)); } } - const orgUnitId = getOrgUnitId(suggestedOrgUnitId, trackerProgram); // can't run rules when no valid organisation unit is specified, i.e. only the registration section will be visible - if (!orgUnitId) { + if (!orgUnit) { return Promise.resolve(initializeRegisterTei(trackerProgram && trackerProgram.id)); } - const orgUnit = { - id: 'DiszpKrYNg8', - name: 'Ngelehun CHC', - code: 'OU_559', - }; return of(initializeRegisterTei(programId, orgUnit)); })); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js new file mode 100644 index 0000000000..9ec22b3a0b --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper/hooks/useDataEntryReduxConverter.js @@ -0,0 +1,148 @@ +// @flow +import { useSelector } from 'react-redux'; +import moment from 'moment'; +import { getDataEntryKey } from '../../../../../DataEntry/common/getDataEntryKey'; +import { getTrackedEntityTypeThrowIfNotFound, getTrackerProgramThrowIfNotFound } from '../../../../../../metaData'; +import type { RenderFoundation } from '../../../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../../../converters'; +import { + convertDataEntryValuesToClientValues, +} from '../../../../../DataEntry/common/convertDataEntryValuesToClientValues'; +import { getFormattedStringFromMomentUsingEuropeanGlyphs } from '../../../../../../../capture-core-utils/date'; +import { capitalizeFirstLetter } from '../../../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../../../utils/uid/generateUID'; + +type DataEntryReduxConverterProps = { + dataEntryId: string; + itemId: string; + trackedEntityTypeId: string; +}; + +function getMetadata(programId: ?string, tetId: string) { + return programId ? getTrackerProgramMetadata(programId) : getTETMetadata(tetId); +} + +function getTrackerProgramMetadata(programId: string) { + const program = getTrackerProgramThrowIfNotFound(programId); + return { + form: program.enrollment.enrollmentForm, + attributes: program.trackedEntityType.attributes, + tetName: program.trackedEntityType.name, + }; +} + +function getTETMetadata(tetId: string) { + const tet = getTrackedEntityTypeThrowIfNotFound(tetId); + return { + form: tet.teiRegistration.form, + attributes: tet.attributes, + tetName: tet.name, + }; +} + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + const clientValues = formFoundation.convertValues(formValues, convertFormToClient); + return clientValues; +} + +function getServerValuesForMainValues( + values: Object, + meta: Object, + formFoundation: RenderFoundation, +) { + const clientValues = convertDataEntryValuesToClientValues( + values, + meta, + formFoundation, + ) || {}; + + // potientally run this through a server to client converter for enrollment, the same way as for event + const serverValues = Object + .keys(clientValues) + .reduce((acc, key) => { + const value = clientValues[key]; + const type = meta[key].type; + acc[key] = convertClientToServer(value, type); + return acc; + }, {}); + + return serverValues; +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +export const useDataEntryReduxConverter = ({ + dataEntryId, + itemId, + trackedEntityTypeId, +}: DataEntryReduxConverterProps) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); + const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); + const { programId, orgUnit } = useSelector(({ newRelationshipRegisterTei }) => newRelationshipRegisterTei); + + const buildTeiPayload = () => { + const { form: formFoundation } = getMetadata(programId, trackedEntityTypeId); + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertValues(clientValues, convertClientToServer); + const serverValuesForMainValues = getServerValuesForMainValues( + dataEntryFieldValues, + dataEntryFieldsMeta, + formFoundation, + ); + + // $FlowFixMe + const attributes = Object.keys(serverValuesForFormValues) + .map(key => ({ + attribute: key, + value: serverValuesForFormValues[key], + })); + + const enrollment = programId ? { + program: programId, + status: 'ACTIVE', + orgUnit: orgUnit.id, + occurredAt: getFormattedStringFromMomentUsingEuropeanGlyphs(moment()), + attributes, + ...serverValuesForMainValues, + } : null; + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + // $FlowFixMe + attributes: !enrollment ? attributes : undefined, + trackedEntity: generateUID(), + orgUnit: orgUnit.id, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: enrollment ? [enrollment] : [], + }; + }; + + + return { + buildTeiPayload, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js index 43f312991b..c772f45b59 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.js @@ -49,14 +49,14 @@ const NewTrackedEntityRelationshipPlain = ({ onSelectFindMode, classes, }: StyledComponentProps) => { + const [currentStep, setCurrentStep] = + useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); + const [selectedLinkedEntityMetadata: LinkedEntityMetadata, setSelectedLinkedEntityMetadata] = useState(undefined); const { mutate } = useAddRelationship({ teiId, - onMutate: () => onSave(), + onMutate: () => onSave && onSave(), }); - const [currentStep, setCurrentStep] = - useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); - const [selectedLinkedEntityMetadata: LinkedEntityMetadata, setSelectedLinkedEntityMetadata] = useState(undefined); const onLinkToTrackedEntityFromSearch = useCallback( (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => { @@ -84,9 +84,11 @@ const NewTrackedEntityRelationshipPlain = ({ } mutate({ - relationship: { - relationshipType: relationshipId, - ...apiData, + apiData: { + relationships: [{ + relationshipType: relationshipId, + ...apiData, + }], }, clientRelationship: { relationshipType: relationshipId, @@ -95,6 +97,37 @@ const NewTrackedEntityRelationshipPlain = ({ }); }, [mutate, selectedLinkedEntityMetadata, teiId]); + const onLinkToTrackedEntityFromRegistration = useCallback((trackedEntity: Object) => { + if (!selectedLinkedEntityMetadata) return; + const { relationshipId, targetSide } = selectedLinkedEntityMetadata; + + const relationshipData = targetSide === TARGET_SIDES.TO ? + { from: { trackedEntity: { trackedEntity: teiId } }, to: { trackedEntity: { trackedEntity: trackedEntity.trackedEntity } } } : + { from: { trackedEntity: { trackedEntity: trackedEntity.trackedEntity } }, to: { trackedEntity: { trackedEntity: teiId } } }; + + const clientData = { + relationshipType: relationshipId, + createdAt: new Date().toISOString(), + ...relationshipData, + [targetSide.toLowerCase()]: { + trackedEntity: { + attributes: trackedEntity.attributes ?? trackedEntity.enrollments?.[0]?.attributes, + orgUnitId: trackedEntity.orgUnit, + trackedEntity: trackedEntity.trackedEntity, + trackedEntityType: trackedEntity.trackedEntityType, + }, + }, + }; + + mutate({ + apiData: { + trackedEntities: [trackedEntity], + relationships: [{ relationshipType: relationshipId, ...relationshipData }], + }, + clientRelationship: clientData, + }); + }, [mutate, selectedLinkedEntityMetadata, teiId]); + const handleNavigation = useCallback( (destination: $Values) => { setCurrentStep(destination); @@ -109,6 +142,7 @@ const NewTrackedEntityRelationshipPlain = ({ if (selectedLinkedEntityMetadata) { onSelectFindMode && onSelectFindMode({ findMode: 'TEI_SEARCH', + orgUnitId, relationshipConstraint: { programId: selectedLinkedEntityMetadata?.programId, trackedEntityTypeId: selectedLinkedEntityMetadata.trackedEntityTypeId, @@ -116,7 +150,7 @@ const NewTrackedEntityRelationshipPlain = ({ }); } setCurrentStep(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.FIND_EXISTING_LINKED_ENTITY); - }, [onSelectFindMode, selectedLinkedEntityMetadata]); + }, [onSelectFindMode, orgUnitId, selectedLinkedEntityMetadata]); const handleNewRetrieverModeSelected = useCallback(() => { if (selectedLinkedEntityMetadata) { @@ -185,7 +219,8 @@ const NewTrackedEntityRelationshipPlain = ({ return renderTrackedEntityRegistration( linkedEntityTrackedEntityTypeId, linkedEntityProgramId, - (...args) => console.log('Registration completed', args), + onLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch, ); } } @@ -195,7 +230,20 @@ const NewTrackedEntityRelationshipPlain = ({ {i18n.t('Missing implementation step')}
); - }, [currentStep, handleLinkedEntityMetadataSelection, handleNewRetrieverModeSelected, handleSearchRetrieverModeSelected, onLinkToTrackedEntityFromSearch, programId, relationshipTypes, renderTrackedEntityRegistration, renderTrackedEntitySearch, selectedLinkedEntityMetadata, trackedEntityTypeId]); + }, [ + currentStep.id, + handleLinkedEntityMetadataSelection, + handleNewRetrieverModeSelected, + handleSearchRetrieverModeSelected, + onLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch, + programId, + relationshipTypes, + renderTrackedEntityRegistration, + renderTrackedEntitySearch, + selectedLinkedEntityMetadata, + trackedEntityTypeId, + ]); return (
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js index 0e55593be0..95f24c4882 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.types.js @@ -1,13 +1,22 @@ // @flow import * as React from 'react'; import type { RelationshipTypes } from '../../common/Types'; -import type { OnLinkToTrackedEntity, OnSelectFindMode } from '../WidgetTrackedEntityRelationship.types'; +import type { + OnLinkToTrackedEntityFromSearch, + OnLinkToTrackedEntityFromRegistration, + OnSelectFindMode, +} from '../WidgetTrackedEntityRelationship.types'; type RenderTrackedEntitySearch = - (trackedEntityTypeId: string, programId: string, onLinkToTrackedEntity: OnLinkToTrackedEntity) => React.Element + (trackedEntityTypeId: string, programId: string, onLinkToTrackedEntity: OnLinkToTrackedEntityFromSearch) => React.Element type RenderTrackedEntityRegistration = - (trackedEntityTypeId: string, programId: string, onLinkToTrackedEntity: OnLinkToTrackedEntity) => React.Element + ( + trackedEntityTypeId: string, + programId: string, + onLinkToTrackedEntityFromRegistration: OnLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntity: OnLinkToTrackedEntityFromSearch, + ) => React.Element export type ContainerProps = $ReadOnly<{| teiId: string, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js index e8405e6142..274072a7d8 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.js @@ -1,6 +1,8 @@ // @flow +import i18n from '@dhis2/d2-i18n'; +// $FlowFixMe - useAlert is exported from app-runtime +import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from 'react-query'; -import { useDataEngine } from '@dhis2/app-runtime'; type Props = { teiId: string; @@ -13,25 +15,37 @@ const ReactQueryAppNamespace = 'capture'; const addRelationshipMutation = { resource: '/tracker?async=false&importStrategy=CREATE', type: 'create', - data: ({ relationship }) => ({ - relationships: [relationship], - }), + data: ({ apiData }) => apiData, }; export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { const queryClient = useQueryClient(); const dataEngine = useDataEngine(); + const { show: showSnackbar } = useAlert( + ({ text }) => text, + ({ barStatus }) => barStatus, + ); + // $FlowFixMe - Is there something wrong with the types? const { mutate } = useMutation( - ({ relationship }) => dataEngine.mutate(addRelationshipMutation, { + ({ apiData }) => dataEngine.mutate(addRelationshipMutation, { variables: { - relationship, + apiData, }, }), { + onError: () => { + queryClient.invalidateQueries([ReactQueryAppNamespace, 'relationships']); + showSnackbar({ + text: i18n.t('An error occurred while adding the relationship'), + barStatus: { critical: true }, + }); + }, onMutate: (...props) => { onMutate && onMutate(...props); const { clientRelationship } = props[0]; + if (!clientRelationship) return; + queryClient.setQueryData([ReactQueryAppNamespace, 'relationships', teiId], (oldData) => { const instances = oldData?.instances || []; const updatedInstances = [clientRelationship, ...instances]; @@ -40,6 +54,10 @@ export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { }, onSuccess: (...props) => { queryClient.invalidateQueries([ReactQueryAppNamespace, 'relationships']); + showSnackbar({ + text: i18n.t('Relationship added'), + barStatus: { success: true }, + }); onSuccess && onSuccess(...props); }, }, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js index ead695a8b2..da87a34f42 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.types.js @@ -8,12 +8,15 @@ export type RelationshipConstraint = {| programId: ?string, |} -export type OnLinkToTrackedEntity = +export type OnLinkToTrackedEntityFromSearch = (linkedTrackedEntityId: string, attributes?: { [attributeId: string]: string }) => void; +export type OnLinkToTrackedEntityFromRegistration = + (teiPayload: Object) => void; + export type OnSelectFindModeProps = {| findMode: string, - orgUnitId?: string, + orgUnitId: string, relationshipConstraint: RelationshipConstraint, |} @@ -33,11 +36,12 @@ export type WidgetTrackedEntityRelationshipProps = {| renderTrackedEntityRegistration: ( trackedEntityTypeId: string, programId: string, - onLinkToTrackedEntity: OnLinkToTrackedEntity, + onLinkToTrackedEntityFromRegistration: OnLinkToTrackedEntityFromRegistration, + onLinkToTrackedEntityFromSearch: OnLinkToTrackedEntityFromSearch, ) => React.Element, renderTrackedEntitySearch?: ( trackedEntityTypeId: string, programId: string, - onLinkToTrackedEntity: OnLinkToTrackedEntity, + onLinkToTrackedEntityFromSearch: OnLinkToTrackedEntityFromSearch, ) => React.Element, |}; diff --git a/src/core_modules/capture-core/utils/uid/generateUID.js b/src/core_modules/capture-core/utils/uid/generateUID.js new file mode 100644 index 0000000000..addcb13f49 --- /dev/null +++ b/src/core_modules/capture-core/utils/uid/generateUID.js @@ -0,0 +1,18 @@ +// @flow + +export const generateUID = (): string => { + const letters = 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const allowedChars = `0123456789${letters}`; + const NUMBER_OF_CODEPOINTS = allowedChars.length; + const CODESIZE = 11; + let uid; + + // the uid should start with a char + uid = letters.charAt(Math.random() * (letters.length)); + + for (let i = 1; i < CODESIZE; ++i) { + uid += allowedChars.charAt(Math.random() * (NUMBER_OF_CODEPOINTS)); + } + + return uid; +};