diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6fc6a44c..cc2c8c1be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [100.42.0](https://github.com/dhis2/capture-app/compare/v100.41.4...v100.42.0) (2023-10-24) + + +### Features + +* [DHIS2-12361] Tracked Entity Relationships widget ([cafed8d](https://github.com/dhis2/capture-app/commit/cafed8d8aac3fe9955739b1a8ee2cdde57722967)) + +## [100.41.4](https://github.com/dhis2/capture-app/compare/v100.41.3...v100.41.4) (2023-10-22) + + +### Bug Fixes + +* **translations:** sync translations from transifex (master) ([c82ce9f](https://github.com/dhis2/capture-app/commit/c82ce9f69159878f5ca9cc2e6ca9944544099368)) + ## [100.41.3](https://github.com/dhis2/capture-app/compare/v100.41.2...v100.41.3) (2023-10-16) diff --git a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js index 7946b94e2e..07636c5de5 100644 --- a/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js +++ b/cypress/e2e/WorkingLists/EventWorkingLists/EventWorkingListsUser/index.js @@ -255,7 +255,7 @@ Then('the list should display data ordered descendingly by report date', () => { cy.get('input[placeholder="To"]').click(); cy.contains('Update') - .click(); + .click({ force: true }); const rows = combineDataAndYear(lastYear, { '01-01': ['14 Female'], diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png new file mode 100644 index 0000000000..59c052056b Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-choose.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png new file mode 100644 index 0000000000..467adef42c Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-existing.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png new file mode 100644 index 0000000000..6e64a2f58d Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add-new.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget-add.png b/docs/user/resources/images/enrollment-dash-relationship-widget-add.png new file mode 100644 index 0000000000..78c0e82f83 Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget-add.png differ diff --git a/docs/user/resources/images/enrollment-dash-relationship-widget.png b/docs/user/resources/images/enrollment-dash-relationship-widget.png new file mode 100644 index 0000000000..baf3c48666 Binary files /dev/null and b/docs/user/resources/images/enrollment-dash-relationship-widget.png differ diff --git a/docs/user/using-the-capture-app.md b/docs/user/using-the-capture-app.md index b335b3e7ce..138568e172 100644 --- a/docs/user/using-the-capture-app.md +++ b/docs/user/using-the-capture-app.md @@ -986,6 +986,39 @@ The enrollment comment widget displays comments and allows addition of comments, By clicking in the text field, you will be able to enter new text and see action buttons **Save comment** and **Cancel**. Note that Enrollment comments are attributed to a user and cannot be deleted. +### Relationship widget + +The Relationships widget on the enrollment dashboard is used for viewing the record’s linked relationships to other records. +The number next to the title signifies the total number of relationships + +![](resources/images/enrollment-dash-relationship-widget.png) + +For tracked entity instance relationships, the key attributes shown in the widget are the attributes that have been selected to be displayed on the relationship type page in Maintenance. + +If no attributes are selected, it will just show a row per record with tracked entity type name and relationship creation date. + +When clicking a tracked entity instance you should be taken to the Enrollment Dashboard. If the relationship type includes a program, you should be taken to the latest enrollment for that program. If no program is specified, you should still be sent to the enrollment dashboard, but without a program. + +Click the **Add new** button to add a new relationship. Adding a new relationship opens a dialog where you can select the applicable relationship type. + +![](resources/images/enrollment-dash-relationship-widget-add.png) + +Choose between linking to an existing tracked entity instance or creating a new one. + +![](resources/images/enrollment-dash-relationship-widget-add-choose.png) + +#### Existing tracked entity instance + +Use the search form to find any existing record to link to. + +![](resources/images/enrollment-dash-relationship-widget-add-existing.png) + +#### New tracked entity instance + +Use the form to create a new record and link. + +![](resources/images/enrollment-dash-relationship-widget-add-new.png) + ### Tracked entity instance profile widget On the enrollment dashboard, you can view the tracked entity instance profile widget. Inside the profile widget you can view the key attributes values. @@ -1226,4 +1259,4 @@ The attribute option combo selector will be displayed when you are adding or cha Example from new Tracker event: -![](resources/images/attribute-option-combo-tracker.png) \ No newline at end of file +![](resources/images/attribute-option-combo-tracker.png) diff --git a/i18n/en.pot b/i18n/en.pot index 06e8de8d33..2396905e88 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-29T14:14:34.330Z\n" -"PO-Revision-Date: 2023-09-29T14:14:34.330Z\n" +"POT-Creation-Date: 2023-09-12T06:24:49.265Z\n" +"PO-Revision-Date: 2023-09-12T06:24:49.265Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -947,6 +947,18 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Selected program" +msgstr "Selected program" + +msgid "Search {{uniqueAttrName}}" +msgstr "Search {{uniqueAttrName}}" + +msgid "Search by attributes" +msgstr "Search by attributes" + +msgid "Could not retrieve metadata. Please try again later." +msgstr "Could not retrieve metadata. Please try again later." + msgid "Possible duplicates found" msgstr "Possible duplicates found" @@ -1010,9 +1022,6 @@ msgstr "Search {{name}}" msgid "Search by {{name}}" msgstr "Search by {{name}}" -msgid "Search by attributes" -msgstr "Search by attributes" - msgid "all programs" msgstr "all programs" @@ -1070,12 +1079,6 @@ msgstr "Missing search criteria" msgid "Results found" msgstr "Results found" -msgid "Selected program" -msgstr "Selected program" - -msgid "Search {{uniqueAttrName}}" -msgstr "Search {{uniqueAttrName}}" - msgid "Saved lists in this program" msgstr "Saved lists in this program" @@ -1359,6 +1362,42 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "Stages and Events" msgstr "Stages and Events" +msgid "New TEI Relationship" +msgstr "New TEI Relationship" + +msgid "Missing implementation step" +msgstr "Missing implementation step" + +msgid "Go back without saving relationship" +msgstr "Go back without saving relationship" + +msgid "New Relationship" +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 "Something went wrong while loading relationships. Please try again later." +msgstr "Something went wrong while loading relationships. Please try again later." + +msgid "{{trackedEntityTypeName}} relationships" +msgstr "{{trackedEntityTypeName}} relationships" + +msgid "To open this relationship, please wait until saving is complete" +msgstr "To open this relationship, please wait until saving is complete" + +msgid "Type" +msgstr "Type" + +msgid "Created date" +msgstr "Created date" + +msgid "Program stage name" +msgstr "Program stage name" + msgid "Working list could not be loaded" msgstr "Working list could not be loaded" diff --git a/i18n/es.po b/i18n/es.po index b74921f8ef..cd154429ff 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -17,7 +17,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n" +"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" "Last-Translator: Enzo Nicolas Rossi , 2023\n" "Language-Team: Spanish (https://app.transifex.com/hisp-uio/teams/100509/es/)\n" @@ -172,6 +172,15 @@ msgstr "Coordenada" msgid "Enrollment" msgstr "Inscripción" +msgid "Complete event" +msgstr "Completar el evento" + +msgid "{{ stageName }} - Basic info" +msgstr "" + +msgid "{{ stageName }} - Status" +msgstr "" + msgid "Please select {{categoryName}}" msgstr "Seleccione {{categoryName}}" @@ -192,15 +201,18 @@ msgstr "Cancelar" msgid "Metadata error. see log for details" msgstr "Error de metadatos. ver registro para más detalles" +msgid "{{ stageName }} - Details" +msgstr "" + +msgid "{{ stageName }} - {{ sectionName }}" +msgstr "" + msgid "Assigned user" msgstr "Usuario asignado" msgid "Search for user" msgstr "Buscar usuario" -msgid "Complete event" -msgstr "Completar el evento" - msgid "Basic info" msgstr "Información básica" @@ -438,13 +450,13 @@ msgid "Days in the future" msgstr "Días en el futuro" msgid "From" -msgstr "De" +msgstr "Desde" msgid "Days in the past" msgstr "Días en el pasado" msgid "To" -msgstr "Para" +msgstr "Hasta" msgid "This week" msgstr "Esta semana" @@ -791,7 +803,7 @@ msgid "There was an error loading the page" msgstr "Hubo un error al cargar la página" msgid "Choose a registering unit to start reporting" -msgstr "Elija una unidad de registro para comenzar a informar" +msgstr "Seleccione una unidad de registro para comenzar a informar" msgid "There are no feedbacks for this event" msgstr "No hay comentarios para este evento." @@ -866,7 +878,7 @@ msgid "View enrollment" msgstr "Ver inscripción" msgid "Create for" -msgstr "Crear para" +msgstr "Crear" msgid "" "You can also choose a program from the top bar and create in that program" diff --git a/i18n/nb.po b/i18n/nb.po index 036767eb33..59b9da637b 100644 --- a/i18n/nb.po +++ b/i18n/nb.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n" +"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" "PO-Revision-Date: 2019-06-27 07:31+0000\n" "Last-Translator: Karoline Tufte Lien , 2023\n" "Language-Team: Norwegian Bokmål (https://app.transifex.com/hisp-uio/teams/100509/nb/)\n" @@ -82,6 +82,7 @@ msgstr "Feil" msgid "" "Plugins are not yet available - Please contact your system administrator" msgstr "" +"Plugins er ikke tilgjengelig ennå - Ta kontakt med systemadministratoren din" msgid "This value is validating" msgstr "Denne verdien validerer" @@ -158,8 +159,17 @@ msgstr "Koordinater" msgid "Enrollment" msgstr "Registrering" +msgid "Complete event" +msgstr "Fullfør hendelse" + +msgid "{{ stageName }} - Basic info" +msgstr "{{ stageName }} - Grunnleggende informasjon" + +msgid "{{ stageName }} - Status" +msgstr "{{ stageName }} - Status" + msgid "Please select {{categoryName}}" -msgstr "" +msgstr "Velg {{categoryName}}" msgid "A future date is not allowed" msgstr "En fremtidig dato er ikke tillatt" @@ -176,15 +186,18 @@ msgstr "Avbryt" msgid "Metadata error. see log for details" msgstr "Metadata feil, se logg for detaljer" +msgid "{{ stageName }} - Details" +msgstr "{{ stageName }} - Detaljer" + +msgid "{{ stageName }} - {{ sectionName }}" +msgstr "{{ stageName }} - {{ sectionName }}" + msgid "Assigned user" msgstr "Tildelt programbruker" msgid "Search for user" msgstr "Søk etter bruker" -msgid "Complete event" -msgstr "Fullfør hendelse" - msgid "Basic info" msgstr "Grunnleggende informasjon" @@ -281,7 +294,7 @@ msgstr "" "relasjon" msgid "Yes, discard changes" -msgstr "" +msgstr "Ja, forkast endringer" msgid "No, cancel" msgstr "Nei, avbryt" @@ -385,6 +398,9 @@ msgid "" "This event has unsaved changes. Leaving this page without saving will lose " "these changes. Are you sure you want to discard unsaved changes?" msgstr "" +"Denne hendelsen har ulagrede endringer. Hvis du forlater denne siden uten å " +"lagre, mister du disse endringene. Er du sikker på at du vil forkaste " +"ulagrede endringer?" msgid "No events to display" msgstr "Ingen hendelser å vise" @@ -498,7 +514,7 @@ msgid "Type to filter options" msgstr "Skriv for å filtrere alternativer" msgid "No match found" -msgstr "" +msgstr "Ingen treff funnet" msgid "Search" msgstr "Søk" @@ -597,13 +613,13 @@ msgid "Write comment" msgstr "Skriv kommentar" msgid "was blanked out and hidden by your last action" -msgstr "" +msgstr "ble tømt og skjult av den siste handlingen din" msgid "Notice" -msgstr "" +msgstr "Merknad" msgid "Close the notice" -msgstr "" +msgstr "Lukk merknaden" msgid "Use new Enrollment dashboard for {{programName}}" msgstr "Bruk nytt registreringsdashbord for {{programName}}" @@ -619,18 +635,30 @@ msgid "" "functionality in Capture is ongoing and will be added in upcoming app " "releases." msgstr "" +"Ved å klikke på \"meld på\" nedenfor, vil du begynne å bruke det nye " +"registreringsdashbordet i Capture-appen for dette Tracker-programmet. For " +"øyeblikket er det noe funksjonalitet fra Tracker Capture som ennå ikke er " +"lagt til, inkludert relasjons- og henvisningsfunksjonalitet. Arbeidet med å " +"inkludere denne Tracker-funksjonaliteten i Capture pågår og vil bli lagt til" +" i kommende apputgivelser." msgid "" "The core team appreciates any feedback on this new functionality which is " "currently being beta tested, please report any issues and feedback in the " "DHIS2 JIRA project." msgstr "" +"Kjerneteamet setter pris på all tilbakemelding på denne nye funksjonaliteten" +" som for øyeblikket blir betatestet, vennligst rapporter eventuelle " +"problemer og tilbakemeldinger i DHIS2 JIRA-prosjektet." msgid "" "Click the button below to opt-in to the new enrollment dashboard " "functionality in the Capture app (beta) for this Tracker program for all " "users." msgstr "" +"Klikk på knappen nedenfor for å melde deg på den nye funksjonen for " +"registreringsdashbord i Capture-appen (beta) for dette Tracker-programmet " +"for alle brukere." msgid "Yes, opt in" msgstr "Ja, meld på" @@ -653,6 +681,8 @@ msgstr "" msgid "" "An error occurred while fetching enrollments. Please enter a valid url." msgstr "" +"Det oppsto en feil under henting av registreringer. Vennligst skriv inn en " +"gyldig url." msgid "Enrollment Dashboard" msgstr "Registreringsdashbord" @@ -694,19 +724,19 @@ msgstr "" "{{programName}} har kategorier. Velg alle kategorier for å se dashbordet." msgid "Invalid enrollment id {{enrollmentId}}." -msgstr "" +msgstr "Ugyldig registrerings-ID {{enrollmentId}}" msgid "Choose an enrollment to view the dashboard." msgstr "Velg en registrering for å se dashbordet." msgid "There are no active enrollments." -msgstr "" +msgstr "Det er ingen aktive registreringer." msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "" +msgstr "Legg til ny registrering for {{teiDisplayName}} i dette programmet." msgid "No access to program owner." -msgstr "" +msgstr "Ingen tilgang til programeier." msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} er ikke registrert i dette programmet." @@ -774,7 +804,7 @@ msgid "Refer" msgstr "Henvis" msgid "You can't add any more {{ programStageName }} events" -msgstr "" +msgstr "Du kan ikke legge til flere {{ programStageName }}-hendelser" msgid "Cancel without saving" msgstr "Avbryt uten å lagre" @@ -839,7 +869,7 @@ msgid "New Enrollment in program{{escape}} {{programName}}" msgstr "Ny registrering i programmet{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" -msgstr "" +msgstr "Lagre {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "Lagre {{trackedEntityName}}" @@ -957,13 +987,13 @@ msgid "Add relationship" msgstr "Legg til relasjon" msgid "No results found for " -msgstr "" +msgstr "Ingen resultater funnet for" msgid "Registering unit" msgstr "Registreringsenhet" msgid "Choose a registering unit" -msgstr "" +msgstr "Velg en registreringsenhet" msgid "Clear selection" msgstr "Fjern valg" @@ -972,13 +1002,13 @@ msgid "No programs available." msgstr "Ingen programmer tilgjengelig." msgid "Search for a program" -msgstr "" +msgstr "Søk for et program" msgid "Some programs are being filtered by the chosen registering unit" -msgstr "" +msgstr "Noen programmer blir filtrert av den valgte registreringsenheten" msgid "Show all programs" -msgstr "" +msgstr "Vis alle programmer" msgid "Choose a program" msgstr "Velg et program" @@ -1055,7 +1085,7 @@ msgid "Cannot search in all programs" msgstr "Kan ikke søke i alle programmer" msgid "Missing search criteria" -msgstr "" +msgstr "Manglende søkekriterier" msgid "Results found" msgstr "Resultater funnet" @@ -1096,23 +1126,27 @@ msgid "open the Tracker Capture app" msgstr "åpne Tracker Capture app" msgid "This program is protected" -msgstr "" +msgstr "Dette programmet er beskyttet" msgid "Reason to check for enrollments" -msgstr "" +msgstr "Grunn til å sjekke for registreringer" msgid "" "Describe the reason you are checking for enrollments in this protected " "program" msgstr "" +"Beskriv årsaken til at du ser etter registreringer i dette beskyttede " +"programmet" msgid "Check for enrollments" -msgstr "" +msgstr "Se etter registreringer" msgid "" "You must provide a reason to check for enrollments in this protected " "program. All activity will be logged." msgstr "" +"Du må oppgi en grunn for å se etter registreringer i dette beskyttede " +"programmet. All aktivitet vil bli logget." msgid "Save comment" msgstr "Lagre kommentar" @@ -1159,6 +1193,8 @@ msgstr "Merk for oppfølging" msgid "Existing dates for auto-generated events will not be updated." msgstr "" +"Eksisterende datoer for automatisk genererte hendelser vil ikke bli " +"oppdatert." msgid "Enrollment date" msgstr "Registreringsdato" @@ -1322,7 +1358,7 @@ msgid "New {{ eventName }} event" msgstr "Ny {{ eventName }} hendelse" msgid "To open this event, please wait until saving is complete" -msgstr "" +msgstr "For å åpne denne hendelsen, vennligst vent til lagringen er fullført" msgid "Show {{ rest }} more" msgstr "Vis {{ rest }} flere" @@ -1388,19 +1424,19 @@ msgid "Choose a program stage to filter by {{label}}" msgstr "Velg en programfase å filtrere etter {{label}}" msgid "Active enrollments" -msgstr "" +msgstr "Aktive registreringer" msgid "Completed enrollments" -msgstr "" +msgstr "Fullførte registreringer" msgid "Cancelled enrollments" -msgstr "" +msgstr "Avbrutte registreringer" msgid "Working list could not be updated" msgstr "Arbeidslisten kunne ikke oppdateres" msgid "an error occurred loading the working lists" -msgstr "" +msgstr "det oppstod en feil ved lasting av arbeidslistene" msgid "an error occurred loading Tracked entity instance lists" msgstr "det oppstod en feil ved lasting av lister over sporede enheter" @@ -1535,7 +1571,7 @@ msgid "Set area" msgstr "Sett område" msgid "Area on map saved" -msgstr "" +msgstr "Området på kartet er lagret" msgid "Compatibility mode" msgstr "Kompabilitetsmodus" diff --git a/package.json b/package.json index dfcf6bde57..9c2c1e3acd 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "capture-app", "homepage": ".", - "version": "100.41.3", - "cacheVersion": "6", + "version": "100.42.0", + "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", "private": true, @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.41.3", + "@dhis2/rules-engine-javascript": "100.42.0", "@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 2280715107..96f533f035 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.41.3", + "version": "100.42.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/src/components/App/AppPages.component.js b/src/components/App/AppPages.component.js index 2203baf361..0af49adb99 100644 --- a/src/components/App/AppPages.component.js +++ b/src/components/App/AppPages.component.js @@ -9,17 +9,21 @@ import { EnrollmentPage } from 'capture-core/components/Pages/Enrollment'; import { StageEventListPage } from 'capture-core/components/Pages/StageEvent'; import { EnrollmentEditEventPage } from 'capture-core/components/Pages/EnrollmentEditEvent'; import { EnrollmentAddEventPage } from 'capture-core/components/Pages/EnrollmentAddEvent'; +import { ReactQueryDevtools } from 'react-query/devtools'; export const AppPages = () => ( - - - - - - - - - - - + <> + + + + + + + + + + + + + ); 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 623a16abce..e16304cf87 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.container.js @@ -5,29 +5,38 @@ 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 { + useBuildEnrollmentPayload, +} from './hooks/useBuildEnrollmentPayload'; export const EnrollmentRegistrationEntry: ComponentType = ({ selectedScopeId, id, saveButtonText, trackedEntityInstanceAttributes, + orgUnitId, + teiId, onSave, ...passOnProps }) => { - const orgUnitId = useCurrentOrgUnitInfo().id; const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); const { - teiId, ready, skipDuplicateCheck, firstStageMetaData, 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 => @@ -41,10 +50,16 @@ export const EnrollmentRegistrationEntry: ComponentType = ({ const isSavingInProgress = useSelector(({ possibleDuplicates, newPage }) => possibleDuplicates.isLoading || possibleDuplicates.isUpdating || !!newPage.uid); + if (error) { return error.errorComponent; } + const onSaveWithEnrollment = () => { + const teiWithEnrollment = buildTeiWithEnrollment(); + onSave(teiWithEnrollment); + }; + return ( = ({ orgUnit={orgUnit} isUserInteractionInProgress={isUserInteractionInProgress} isSavingInProgress={isSavingInProgress} - onSave={() => onSave(formFoundation, firstStageMetaData)} + onSave={onSaveWithEnrollment} /> ); }; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js index f1924c00b3..d58a42aff2 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types.js @@ -8,8 +8,32 @@ 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, selectedScopeId: string, fieldOptions?: Object, onSave: SaveForDuplicateCheck, diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js new file mode 100644 index 0000000000..95778162b0 --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useBuildEnrollmentPayload.js @@ -0,0 +1,188 @@ +// @flow +import { useSelector } from 'react-redux'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import { + getTrackerProgramThrowIfNotFound, + Section, +} from '../../../../metaData'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { + convertDataEntryValuesToClientValues, +} from '../../../DataEntry/common/convertDataEntryValuesToClientValues'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { + useBuildFirstStageRegistration, +} from './useBuildFirstStageRegistration'; +import { + useMetadataForRegistrationForm, +} from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { + useMergeFormFoundationsIfApplicable, +} from './useMergeFormFoundationsIfApplicable'; +import { + deriveAutoGenerateEvents, + deriveFirstStageDuringRegistrationEvent, +} from '../../../Pages/New/RegistrationDataEntry/helpers'; +import { FEATURETYPE } from '../../../../constants'; +import type { EnrollmentPayload } from '../EnrollmentRegistrationEntry.types'; + +type DataEntryReduxConverterProps = { + programId: string; + dataEntryId: string; + itemId?: string; + orgUnitId: string; + teiId: ?string; + trackedEntityTypeId: string; +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + const clientValues = formFoundation.convertValues(formValues, convertFormToClient); + return clientValues; +} + +function getServerValuesForMainValues( + values: Object, + meta: Object, + formFoundation: RenderFoundation, +) { + const clientValues = convertDataEntryValuesToClientValues( + values, + meta, + formFoundation, + ) || {}; + + // potientally run this through a server to client converter for enrollment, the same way as for event + const serverValues = Object + .keys(clientValues) + .reduce((acc, key) => { + const value = clientValues[key]; + const type = meta[key].type; + acc[key] = convertClientToServer(value, type); + return acc; + }, {}); + + return serverValues; +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); + +const deriveAttributesFromFormValues = (formValues = {}) => + Object.keys(formValues) + .filter(key => !geometryType(key)) + .map<{ attribute: string, value: ?any }>(key => ({ attribute: key, value: formValues[key] })); + +export const useBuildEnrollmentPayload = ({ + programId, + dataEntryId, + itemId = 'newEnrollment', + orgUnitId, + teiId, + trackedEntityTypeId, +}: DataEntryReduxConverterProps) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + const dataEntryFieldValues = useSelector(({ dataEntriesFieldsValue }) => dataEntriesFieldsValue[dataEntryKey]); + const dataEntryFieldsMeta = useSelector(({ dataEntriesFieldsMeta }) => dataEntriesFieldsMeta[dataEntryKey]); + const { formFoundation: scopeFormFoundation } = useMetadataForRegistrationForm({ selectedScopeId: programId }); + const { firstStageMetaData } = useBuildFirstStageRegistration(programId); + const { formFoundation } = useMergeFormFoundationsIfApplicable(scopeFormFoundation, firstStageMetaData); + + const buildTeiWithEnrollment = (): EnrollmentPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const firstStage = firstStageMetaData && firstStageMetaData.stage; + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertAndGroupBySection(clientValues, convertClientToServer); + const serverValuesForMainValues = getServerValuesForMainValues( + dataEntryFieldValues, + dataEntryFieldsMeta, + formFoundation, + ); + const { enrolledAt, occurredAt } = serverValuesForMainValues; + + const { stages } = getTrackerProgramThrowIfNotFound(programId); + + const attributeCategoryOptionsId = 'attributeCategoryOptions'; + const attributeCategoryOptions = Object.keys(serverValuesForMainValues) + .filter(key => key.startsWith(attributeCategoryOptionsId)) + .reduce((acc, key) => { + const categoryId = key.split('-')[1]; + acc[categoryId] = serverValuesForMainValues[key]; + return acc; + }, {}); + + const formServerValues = serverValuesForFormValues[Section.groups.ENROLLMENT]; + const currentEventValues = serverValuesForFormValues[Section.groups.EVENT]; + + + const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ + firstStageMetadata: firstStage, + programId, + orgUnitId, + currentEventValues, + fieldsValue: dataEntryFieldValues, + attributeCategoryOptions, + }); + + const autoGenerateEvents = deriveAutoGenerateEvents({ + firstStageMetadata: firstStage, + stages, + enrolledAt, + occurredAt, + programId, + orgUnitId, + attributeCategoryOptions, + }); + + const allEventsToBeCreated = firstStageDuringRegistrationEvent + ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] + : autoGenerateEvents; + + const enrollment = { + program: programId, + status: 'ACTIVE', + orgUnit: orgUnitId, + occurredAt, + enrolledAt, + attributes: deriveAttributesFromFormValues(formServerValues), + events: allEventsToBeCreated, + }; + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + trackedEntity: teiId || generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [enrollment], + }; + }; + + return { + buildTeiWithEnrollment, + }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js index 0c4a280bf6..4a84d7ccb7 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/hooks/useLifecycle.js @@ -4,7 +4,6 @@ import { useEffect, useRef } from 'react'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import { startNewEnrollmentDataEntryInitialisation } from '../EnrollmentRegistrationEntry.actions'; import { scopeTypes, getProgramThrowIfNotFound } from '../../../../metaData'; -import { useLocationQuery } from '../../../../utils/routing'; import { useScopeInfo } from '../../../../hooks/useScopeInfo'; import { useFormValues } from './index'; import type { InputAttribute } from './useFormValues'; @@ -18,8 +17,9 @@ export const useLifecycle = ( dataEntryId: string, trackedEntityInstanceAttributes?: Array, orgUnit: ?OrgUnit, + teiId: ?string, + programId: string, ) => { - const { teiId, 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 e35cfbb4ce..4c3b8badab 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.container.js @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import React, { useEffect, useMemo } from 'react'; import type { ComponentType } from 'react'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { useCurrentOrgUnitInfo } from '../../../hooks/useCurrentOrgUnitInfo'; import { Enrollment, scopeTypes } from '../../../metaData'; import { startNewTeiDataEntryInitialisation } from './TeiRegistrationEntry.actions'; import type { OwnProps } from './TeiRegistrationEntry.types'; @@ -11,11 +10,11 @@ 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) => { +const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId, orgUnitId) => { const dispatch = useDispatch(); const { scopeType, trackedEntityName } = useScopeInfo(selectedScopeId); - const { id: selectedOrgUnitId } = useCurrentOrgUnitInfo(); const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const formValues = useFormValuesFromSearchTerms(); const registrationFormReady = !!formId; @@ -24,18 +23,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { if (registrationFormReady && scopeType === scopeTypes.TRACKED_ENTITY_TYPE) { dispatch( startNewTeiDataEntryInitialisation( - { selectedOrgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, + { selectedOrgUnitId: orgUnitId, selectedScopeId, dataEntryId, formFoundation, formValues }, )); } }, [ scopeType, dataEntryId, selectedScopeId, - selectedOrgUnitId, registrationFormReady, formFoundation, formValues, dispatch, + orgUnitId, ]); return { @@ -44,13 +43,18 @@ const useInitialiseTeiRegistration = (selectedScopeId, dataEntryId) => { }; -export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, ...rest }) => { - const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id); +export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, id, orgUnitId, onSave, ...rest }) => { + const { trackedEntityName } = useInitialiseTeiRegistration(selectedScopeId, id, orgUnitId); const ready = useSelector(({ dataEntries }) => (!!dataEntries[id])); const dataEntry = useSelector(({ dataEntries }) => (dataEntries[id])); const { registrationMetaData: teiRegistrationMetadata, } = useMetadataForRegistrationForm({ selectedScopeId }); + const { buildTeiWithoutEnrollment } = useBuildTeiPayload({ + trackedEntityTypeId: selectedScopeId, + dataEntryId: id, + orgUnitId, + }); const dataEntryKey = useMemo(() => { if (dataEntry) { @@ -68,14 +72,21 @@ export const TeiRegistrationEntry: ComponentType = ({ selectedScopeId, return null; } + const onSaveWithoutEnrollment = () => { + const teiPayload = buildTeiWithoutEnrollment(); + onSave(teiPayload); + }; + return ( ); diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js index d73d49c473..43afd9adc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.types.js @@ -2,23 +2,27 @@ 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, |}>; type ContainerProps = {| + orgUnitId: string, teiRegistrationMetadata: RegistrationFormMetadata, ready: boolean, trackedEntityName: string, @@ -37,9 +41,9 @@ type PropsAddedInHOC = {| |}; type PropsRemovedInHOC = {| renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, duplicatesReviewPageSize: number, - onSave: SaveForDuplicateCheck, + onSave: (TeiPayload) => void, |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js new file mode 100644 index 0000000000..0c4cd6da0d --- /dev/null +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/hooks/useBuildTeiPayload.js @@ -0,0 +1,81 @@ +// @flow +import { useSelector } from 'react-redux'; +import { useMetadataForRegistrationForm } from '../../common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import type { RenderFoundation } from '../../../../metaData'; +import { convertClientToServer, convertFormToClient } from '../../../../converters'; +import { capitalizeFirstLetter } from '../../../../../capture-core-utils/string'; +import { generateUID } from '../../../../utils/uid/generateUID'; +import { getDataEntryKey } from '../../../DataEntry/common/getDataEntryKey'; +import type { + TeiPayload, +} from '../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; + +type Props = { + trackedEntityTypeId: string, + dataEntryId: string, + orgUnitId: string, + itemId?: string, +}; + +function getClientValuesForFormData(formValues: Object, formFoundation: RenderFoundation) { + return formFoundation.convertValues(formValues, convertFormToClient); +} + +function getPossibleTetFeatureTypeKey(serverValues: Object) { + return Object + .keys(serverValues) + .find(key => key.startsWith('FEATURETYPE_')); +} + +function buildGeometryProp(key: string, serverValues: Object) { + if (!serverValues[key]) { + return undefined; + } + const type = capitalizeFirstLetter(key.replace('FEATURETYPE_', '').toLocaleLowerCase()); + return { + type, + coordinates: serverValues[key], + }; +} + +export const useBuildTeiPayload = ({ + trackedEntityTypeId, + dataEntryId, + itemId = 'newTei', + orgUnitId, +}: Props) => { + const dataEntryKey = getDataEntryKey(dataEntryId, itemId); + const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId: trackedEntityTypeId }); + const formValues = useSelector(({ formsValues }) => formsValues[dataEntryKey]); + + const buildTeiWithoutEnrollment = (): TeiPayload => { + if (!formFoundation) throw Error('form foundation object not found'); + const clientValues = getClientValuesForFormData(formValues, formFoundation); + const serverValuesForFormValues = formFoundation.convertValues(clientValues, convertClientToServer); + + // $FlowFixMe + const attributes = Object.keys(serverValuesForFormValues) + .map(key => ({ + attribute: key, + value: serverValuesForFormValues[key], + })); + + const tetFeatureTypeKey = getPossibleTetFeatureTypeKey(serverValuesForFormValues); + let geometry; + if (tetFeatureTypeKey) { + geometry = buildGeometryProp(tetFeatureTypeKey, serverValuesForFormValues); + delete serverValuesForFormValues[tetFeatureTypeKey]; + } + + return { + attributes, + trackedEntity: generateUID(), + orgUnit: orgUnitId, + trackedEntityType: trackedEntityTypeId, + geometry, + enrollments: [], + }; + }; + + return { buildTeiWithoutEnrollment }; +}; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js index a727db9ec2..f55dc39c0e 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/types/duplicateCheckOnSave.types.js @@ -1,7 +1,7 @@ // @flow -import { ProgramStage, RenderFoundation } from '../../../../../../metaData'; +import type { EnrollmentPayload } from '../../../../EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { TeiPayload } from '../../../../../Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type SaveForDuplicateCheck = ( - formFoundation?: RenderFoundation, - firstStageMetaData?: { stage: ProgramStage }, + teiWithEnrollment: EnrollmentPayload | TeiPayload, ) => void; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js index 98525d23d5..d74476bfee 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/useDuplicateCheckerOnSave.types.js @@ -1,9 +1,8 @@ // @flow import { type InputSearchGroup } from '../../../../../metaData'; -import type { SaveForDuplicateCheck } from './types'; export type Input = {| - onSave: SaveForDuplicateCheck, + onSave: () => void, hasDuplicate: ?boolean, onResetPossibleDuplicates: () => void, onReviewDuplicates: (duplicatesReviewPageSize: number) => void, diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js index 87a3d4cffc..560dd0e9a2 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/DuplicateCheckOnSave/withDuplicateCheckOnSave.types.js @@ -2,16 +2,15 @@ import type { Node } from 'react'; import type { Enrollment, TeiRegistration } from '../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../CardList'; -import type { SaveForDuplicateCheck } from './types'; export type Props = { id: string, selectedScopeId: string, - onSave: SaveForDuplicateCheck, + onSave: () => void, enrollmentMetadata?: Enrollment, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForDuplicateCheck) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: () => void) => Node, skipDuplicateCheck: ?boolean, }; diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js index 49a7b484be..0238087811 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useEnrollmentFormFoundation.js @@ -30,6 +30,7 @@ export const useEnrollmentFormFoundation = ({ locale, }: Props) => { const { data: enrollment, isLoading, error } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['enrollmentForm', program?.id], () => buildEnrollmentForm({ // $FlowFixMe - Flow does not understand that the values are not null here diff --git a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js index ae3d4c3e5a..05a173cfc8 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js +++ b/src/core_modules/capture-core/components/DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm/hooks/useTrackedEntityTypeCollection.js @@ -26,6 +26,7 @@ export const useTrackedEntityTypeCollection = ({ locale, }: Props): ReturnValues => { const { data: trackedEntityAttributes } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityAttributes', trackedEntityType?.id], () => getTrackedEntityAttributes( trackedEntityType @@ -40,6 +41,7 @@ export const useTrackedEntityTypeCollection = ({ ); const { data: trackedEntityTypeCollection } = useIndexedDBQuery( + // $FlowFixMe - QueryKey can be undefined ['trackedEntityTypeCollection', trackedEntityType?.id], () => buildTrackedEntityTypeCollection({ // $FlowFixMe diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js index 99857025f9..6d905e0188 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPage.epics.js @@ -207,3 +207,4 @@ export const openEnrollmentPageEpic = (action$: InputObservable, store: ReduxSto }, ), ); + diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js index 0759fcd558..05be58a16e 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.component.js @@ -1,5 +1,5 @@ // @flow -import React, { type ComponentType } from 'react'; +import React, { type ComponentType, useState, useCallback } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import { spacersNum, spacers, colors } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; @@ -13,8 +13,15 @@ import { WidgetError } from '../../../WidgetErrorAndWarning/WidgetError'; import { WidgetIndicator } from '../../../WidgetIndicator'; import { WidgetEnrollmentComment } from '../../../WidgetEnrollmentComment'; import { EnrollmentQuickActions } from './EnrollmentQuickActions'; +import { + TrackedEntityRelationshipsWrapper, +} from '../../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from '../../EnrollmentEditEvent/AddRelationshipRefWrapper'; const getStyles = () => ({ + container: { + position: 'relative', + }, columns: { display: 'flex', }, @@ -59,68 +66,96 @@ export const EnrollmentPageDefaultPlain = ({ hideWidgets, classes, onEventClick, + onLinkedRecordClick, onUpdateTeiAttributeValues, onUpdateEnrollmentDate, onUpdateIncidentDate, onEnrollmentError, ruleEffects, -}: PlainProps) => ( - <> -
{i18n.t('Enrollment Dashboard')}
-
-
- - -
-
- - - - {!hideWidgets.indicator && ( - - )} - {!hideWidgets.feedback && ( - - )} - - {enrollmentId !== 'AUTO' && } +}: PlainProps) => { + const [mainContentVisible, setMainContentVisibility] = useState(true); + const [addRelationShipContainerElement, setAddRelationshipContainerElement] = + useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisibility(current => !current), []); + + return ( + <> + +
+
{i18n.t('Enrollment Dashboard')}
+
+
+ + +
+
+ {addRelationShipContainerElement && + {}} + onOpenAddRelationship={toggleVisibility} + onCloseAddRelationship={toggleVisibility} + teiId={teiId} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + + + {!hideWidgets.indicator && ( + + )} + {!hideWidgets.feedback && ( + + )} + + {enrollmentId !== 'AUTO' && } +
+
-
- -); + + ); +}; export const EnrollmentPageDefaultComponent: ComponentType = withStyles( diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js index 2e7b8da1bc..d7f3ccdf85 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.js @@ -28,12 +28,14 @@ import { } from './hooks'; import { buildUrlQueryString, useLocationQuery } from '../../../../utils/routing'; import { useFilteredWidgetData } from './hooks/useFilteredWidgetData'; +import { useLinkedRecordClick } from '../../common/TEIRelationshipsWidget'; export const EnrollmentPageDefault = () => { const history = useHistory(); const dispatch = useDispatch(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { orgUnit, error } = useRulesEngineOrgUnit(orgUnitId); + const { onLinkedRecordClick } = useLinkedRecordClick(); const program = useTrackerProgram(programId); const { @@ -102,6 +104,7 @@ export const EnrollmentPageDefault = () => { }; const onEnrollmentError = message => dispatch(showEnrollmentError({ message })); + if (error) { return error.errorComponent; } @@ -122,6 +125,7 @@ export const EnrollmentPageDefault = () => { widgetEffects={outputEffects} hideWidgets={hideWidgets} onEventClick={onEventClick} + onLinkedRecordClick={onLinkedRecordClick} onUpdateTeiAttributeValues={onUpdateTeiAttributeValues} onUpdateEnrollmentDate={onUpdateEnrollmentDate} onUpdateIncidentDate={onUpdateIncidentDate} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js index 4e88f4a39b..143b9b8c28 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types.js @@ -1,12 +1,13 @@ // @flow import { typeof effectActions } from '@dhis2/rules-engine-javascript'; -import type { Program } from 'capture-core/metaData'; +import type { TrackerProgram } from 'capture-core/metaData'; import type { Stage } from 'capture-core/components/WidgetStagesAndEvents/types/common.types'; import type { WidgetEffects, HideWidgets } from '../../common/EnrollmentOverviewDomain'; import type { Event } from '../../common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData'; +import type { LinkedRecordClick } from '../../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type Props = {| - program: Program, + program: TrackerProgram, enrollmentId: string, teiId: string, events: ?Array, @@ -20,6 +21,7 @@ export type Props = {| onCreateNew: (stageId: string) => void, onEventClick: (eventId: string) => void, onUpdateTeiAttributeValues: (attributes: Array<{ [key: string]: string }>, teiDisplayName: string) => void, + onLinkedRecordClick: LinkedRecordClick, onUpdateEnrollmentDate: (enrollmentDate: string) => void, onUpdateIncidentDate: (incidentDate: string) => void, onEnrollmentError: (message: string) => void, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js new file mode 100644 index 0000000000..7235bb1a14 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/AddRelationshipRefWrapper.component.js @@ -0,0 +1,21 @@ +// @flow +import React, { useEffect, useRef } from 'react'; + +type Props = { + setRelationshipRef: (HTMLDivElement) => void, +} + +export const AddRelationshipRefWrapper = ({ setRelationshipRef }: Props) => { + const renderRelationshipRef = useRef(undefined); + + // Extracting the logic to separate component because of the OrgUnitFetcher + useEffect(() => { + if (renderRelationshipRef.current) { + setRelationshipRef(renderRelationshipRef.current); + } + }, [setRelationshipRef]); + + return ( +
+ ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js new file mode 100644 index 0000000000..7400ed7ff3 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/AddRelationshipRefWrapper/index.js @@ -0,0 +1,3 @@ +// @flow + +export { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper.component'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js index 1add434b21..6137c49b32 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useCallback, useState } from 'react'; import type { ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; @@ -18,12 +18,19 @@ import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; import { WidgetEventComment } from '../../WidgetEventComment'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; +import { + TrackedEntityRelationshipsWrapper, +} from '../common/TEIRelationshipsWidget/TrackedEntityRelationshipsWrapper'; +import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import { NoticeBox } from '../../NoticeBox'; const styles = ({ typography }) => ({ page: { margin: spacersNum.dp16, }, + addRelationshipContainer: { + margin: spacersNum.dp16, + }, columns: { display: 'flex', }, @@ -52,6 +59,7 @@ const EnrollmentEditEventPagePain = ({ programStage, teiId, enrollmentId, + trackedEntityTypeId, programId, enrollmentsAsOptions, trackedEntityName, @@ -62,6 +70,7 @@ const EnrollmentEditEventPagePain = ({ onAddNew, classes, onGoBack, + onLinkedRecordClick, orgUnitId, eventDate, scheduleDate, @@ -71,85 +80,111 @@ const EnrollmentEditEventPagePain = ({ onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, -}: PlainProps) => ( - - -
-
- {mode === dataEntryKeys.VIEW - ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) - : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +}: PlainProps) => { + const [mainContentVisible, setMainContentVisible] = useState(true); + const [addRelationShipContainerElement, setAddRelationShipContainerElement] = useState(undefined); + + const toggleVisibility = useCallback(() => setMainContentVisible(current => !current), []); + + return ( + + +
+
-
-
- {pageStatus === pageStatuses.DEFAULT && programStage && ( - +
+ {mode === dataEntryKeys.VIEW + ? i18n.t('Enrollment{{escape}} View Event', { escape: ':' }) + : i18n.t('Enrollment{{escape}} Edit Event', { escape: ':' })} +
+
+
+ {pageStatus === pageStatuses.DEFAULT && programStage && ( + + )} + {pageStatus === pageStatuses.MISSING_DATA && ( + {i18n.t('The enrollment event data could not be found')} + )} + {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( + + {i18n.t('Choose a registering unit to start reporting')} + + )} +
+
+ + + + {!hideWidgets.feedback && ( + + )} + {!hideWidgets.indicator && ( + + )} + {addRelationShipContainerElement && + {}} + onLinkedRecordClick={onLinkedRecordClick} + /> + } + + - )} - {pageStatus === pageStatuses.MISSING_DATA && ( - {i18n.t('The enrollment event data could not be found')} - )} - {pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED && ( - - {i18n.t('Choose a registering unit to start reporting')} - - )} -
-
- - - - {!hideWidgets.feedback && ( - - )} - {!hideWidgets.indicator && ( - - )} - - +
+
- -
-
-); + + ); +}; export const EnrollmentEditEventPageComponent: ComponentType<$Diff> = withStyles(styles)(EnrollmentEditEventPagePain); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js index b6a27e92bf..82ee3b9360 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.container.js @@ -21,6 +21,7 @@ import { useEvent } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; import { cleanUpDataEntry } from '../../DataEntry'; +import { useLinkedRecordClick } from '../common/TEIRelationshipsWidget'; import { pageKeys } from '../../App/withAppUrlSync'; import { withErrorMessageHandler } from '../../../HOC'; @@ -90,6 +91,8 @@ const EnrollmentEditEventPageWithContextPlain = ({ const history = useHistory(); const dispatch = useDispatch(); + const { onLinkedRecordClick } = useLinkedRecordClick(); + useEffect(() => () => { dispatch(cleanUpDataEntry(dataEntryIds.ENROLLMENT_EVENT)); }, [dispatch]); @@ -116,6 +119,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ } }, [initMode, enrollmentId, eventId, orgUnitId, history]); + const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -123,10 +127,9 @@ const EnrollmentEditEventPageWithContextPlain = ({ dispatch(updateEnrollmentEvents(eventId, eventData)); history.push(`enrollment?${buildUrlQueryString({ enrollmentId })}`); }; - const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; const { teiDisplayName } = useTeiDisplayName(teiId, programId); // $FlowFixMe - const trackedEntityName = program?.trackedEntityType?.name; + const { name: trackedEntityName, id: trackedEntityTypeId } = program?.trackedEntityType; const enrollmentsAsOptions = buildEnrollmentsAsOptions([enrollmentSite || {}], programId); const event = enrollmentSite?.events?.find(item => item.event === eventId); const eventDate = getEventDate(event); @@ -135,6 +138,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); + const pageStatus = getPageStatus({ orgUnitId, enrollmentSite, @@ -154,6 +158,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ hideWidgets={hideWidgets} teiId={teiId} enrollmentId={enrollmentId} + trackedEntityTypeId={trackedEntityTypeId} enrollmentsAsOptions={enrollmentsAsOptions} teiDisplayName={teiDisplayName} trackedEntityName={trackedEntityName} @@ -162,6 +167,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ onAddNew={onAddNew} orgUnitId={orgUnitId} eventDate={eventDate} + onLinkedRecordClick={onLinkedRecordClick} onEnrollmentError={onEnrollmentError} onEnrollmentSuccess={onEnrollmentSuccess} eventStatus={event?.status} 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 8e16b09325..8f269fed00 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 @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../../metaData'; import type { WidgetEffects, HideWidgets } from '../common/EnrollmentOverviewDomain'; +import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; export type PlainProps = {| programStage: ?ProgramStage, @@ -9,6 +10,7 @@ export type PlainProps = {| teiId: string, enrollmentId: string, programId: string, + trackedEntityTypeId: string, mode: string, orgUnitId: string, trackedEntityName: string, @@ -19,6 +21,7 @@ export type PlainProps = {| onDelete: () => void, onAddNew: () => void, onGoBack: () => void, + onLinkedRecordClick: LinkedRecordClick, onEnrollmentError: (message: string) => void, onEnrollmentSuccess: () => void, onCancelEditEvent: (isScheduled: boolean) => void, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js index e716f71741..6beb555bc6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.js @@ -1,7 +1,12 @@ // @flow -import type { ProgramStage, RenderFoundation } from '../../../../metaData'; import { actionCreator } from '../../../../actions/actions.utils'; import { effectMethods } from '../../../../trackerOffline'; +import type { + EnrollmentPayload, +} from '../../../DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.types'; +import type { + TeiPayload, +} from '../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export const registrationFormActionTypes = { NEW_TRACKED_ENTITY_INSTANCE_SAVE_START: 'StartSavingNewTrackedEntityInstance', @@ -16,8 +21,8 @@ export const registrationFormActionTypes = { }; // without enrollment -export const startSavingNewTrackedEntityInstance = (formFoundation: RenderFoundation) => - actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ formFoundation }); +export const startSavingNewTrackedEntityInstance = (teiPayload: TeiPayload) => + actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START)({ teiPayload }); export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE)( @@ -41,11 +46,9 @@ export const saveNewTrackedEntityInstance = (candidateForRegistration: any) => ); // with enrollment -export const startSavingNewTrackedEntityInstanceWithEnrollment = (formFoundation: RenderFoundation, teiId: string, uid: string, firstStage?: ProgramStage) => +export const startSavingNewTrackedEntityInstanceWithEnrollment = (enrollmentPayload: EnrollmentPayload, uid: string) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START)({ - formFoundation, - teiId, - firstStage, + enrollmentPayload, uid, }); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js index cf14636450..d5d956f29d 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.component.js @@ -15,9 +15,7 @@ import { ResultsPageSizeContext } from '../../shared-contexts'; import { navigateToEnrollmentOverview } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { useLocationQuery } from '../../../../utils/routing'; import { EnrollmentRegistrationEntryWrapper } from '../EnrollmentRegistrationEntryWrapper.component'; -import { - useMetadataForRegistrationForm, -} from '../../../DataEntries/common/TEIAndEnrollment/useMetadataForRegistrationForm'; +import { useCurrentOrgUnitInfo } from '../../../../hooks/useCurrentOrgUnitInfo'; const getStyles = ({ typography }) => ({ container: { @@ -99,7 +97,7 @@ const RegistrationDataEntryPlain = ({ const { resultsPageSize } = useContext(ResultsPageSizeContext); const { scopeType, programName, trackedEntityName } = useScopeInfo(selectedScopeId); const titleText = useScopeTitleText(selectedScopeId); - const { formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); + const { id: reduxOrgUnitId } = useCurrentOrgUnitInfo(); const handleRegistrationScopeSelection = (id) => { setScopeId(id); @@ -178,10 +176,10 @@ const RegistrationDataEntryPlain = ({ - onSaveWithEnrollment(customFormFoundation, firstStageMetaData?.stage) - } + onSave={onSaveWithEnrollment} saveButtonText={(trackedEntityTypeNameLC: string) => i18n.t('Save {{trackedEntityTypeName}}', { trackedEntityTypeName: trackedEntityTypeNameLC, interpolation: { escapeValue: false }, @@ -233,11 +231,12 @@ const RegistrationDataEntryPlain = ({ onSaveWithoutEnrollment(formFoundation)} + onSave={onSaveWithoutEnrollment} duplicatesReviewPageSize={resultsPageSize} renderDuplicatesDialogActions={renderDuplicatesDialogActions} renderDuplicatesCardActions={renderDuplicatesCardActions} diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js index 41f5a864d5..8aeda04f1f 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.container.js @@ -26,15 +26,15 @@ export const RegistrationDataEntry: ComponentType = ({ const { teiId } = useLocationQuery(); const dispatchOnSaveWithoutEnrollment = useCallback( - (formFoundation) => { dispatch(startSavingNewTrackedEntityInstance(formFoundation)); }, + (teiPayload) => { dispatch(startSavingNewTrackedEntityInstance(teiPayload)); }, [dispatch]); const dispatchOnSaveWithEnrollment = useCallback( - (formFoundation, firstStage) => { + (enrollmentPayload) => { const uid = uuid(); - dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(formFoundation, teiId, uid, firstStage)); + dispatch(startSavingNewTrackedEntityInstanceWithEnrollment(enrollmentPayload, uid)); }, - [dispatch, teiId]); + [dispatch]); const dataEntryIsReady = useSelector(({ dataEntries }) => (!!dataEntries[dataEntryId])); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js index 004adb75f7..be02acbcb5 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.js @@ -1,59 +1,31 @@ // @flow import { ofType } from 'redux-observable'; -import { pipe } from 'capture-core-utils'; import { flatMap, map } from 'rxjs/operators'; import { of, EMPTY } from 'rxjs'; -import { FEATURETYPE, dataEntryKeys } from 'capture-core/constants'; +import { dataEntryKeys } from 'capture-core/constants'; import { registrationFormActionTypes, saveNewTrackedEntityInstance, saveNewTrackedEntityInstanceWithEnrollment, } from './RegistrationDataEntry.actions'; -import { getTrackerProgramThrowIfNotFound, dataElementTypes, Section } from '../../../../metaData'; +import { getTrackerProgramThrowIfNotFound } from '../../../../metaData'; import { navigateToEnrollmentOverview, } from '../../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; -import { convertFormToClient, convertClientToServer } from '../../../../converters'; import { buildUrlQueryString, shouldUseNewDashboard } from '../../../../utils/routing'; import { - deriveAutoGenerateEvents, - deriveFirstStageDuringRegistrationEvent, getStageWithOpenAfterEnrollment, - standardGeoJson, PAGES, } from './helpers'; -const convertFn = pipe(convertFormToClient, convertClientToServer); - -const geometryType = formValuesKey => Object.values(FEATURETYPE).find(geometryKey => geometryKey === formValuesKey); - -const deriveAttributesFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => !geometryType(key)) - .map(key => ({ attribute: key, value: formValues[key] })); - -const deriveGeometryFromFormValues = (formValues = {}) => - Object.keys(formValues) - .filter(key => geometryType(key)) - .reduce((acc, currentKey) => (standardGeoJson(formValues[currentKey])), undefined); - -export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable, store: ReduxStore) => +export const startSavingNewTrackedEntityInstanceEpic: Epic = (action$: InputObservable) => action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_START), map((action) => { - const { currentSelections: { orgUnitId, trackedEntityTypeId }, formsValues } = store.value; - const values = formsValues['newPageDataEntryId-newTei']; - const formFoundation = action.payload?.formFoundation; - const formServerValues = formFoundation?.convertValues(values, convertFn); + const { teiPayload } = action.payload; return saveNewTrackedEntityInstance( { - trackedEntities: [{ - attributes: deriveAttributesFromFormValues(formServerValues), - geometry: deriveGeometryFromFormValues(values), - enrollments: [], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityTypeId, - }], + trackedEntities: [teiPayload], }); }), ); @@ -80,78 +52,26 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( action$.pipe( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START), map((action) => { - const formId = 'newPageDataEntryId-newEnrollment'; - const { currentSelections: { orgUnitId, programId }, formsValues, dataEntriesFieldsValue } = store.value; + const { currentSelections: { programId } } = store.value; const { dataStore, userDataStore, temp } = store.value.useNewDashboard; - const { formFoundation, teiId: trackedEntity, firstStage: firstStageMetadata, uid } = action.payload; - const fieldsValue = dataEntriesFieldsValue[formId] || {}; - const { occurredAt, enrolledAt, geometry } = fieldsValue; - const attributeCategoryOptionsId = 'attributeCategoryOptions'; - const attributeCategoryOptions = Object.keys(fieldsValue) - .filter(key => key.startsWith(attributeCategoryOptionsId)) - .reduce((acc, key) => { - const categoryId = key.split('-')[1]; - acc[categoryId] = fieldsValue[key]; - return acc; - }, {}); - const { trackedEntityType, stages } = getTrackerProgramThrowIfNotFound(programId); - const currentFormData = formsValues[formId] || {}; + const { enrollmentPayload, uid } = action.payload; + const { stages, useFirstStageDuringRegistration } = getTrackerProgramThrowIfNotFound(programId); + const shouldRedirect = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); const { stageWithOpenAfterEnrollment, redirectTo } = getStageWithOpenAfterEnrollment( stages, - firstStageMetadata, + useFirstStageDuringRegistration, shouldRedirect, ); - const convertedValues = formFoundation.convertAndGroupBySection(currentFormData, convertFn); - const formServerValues = convertedValues[Section.groups.ENROLLMENT]; - const currentEventValues = convertedValues[Section.groups.EVENT]; - - const firstStageDuringRegistrationEvent = deriveFirstStageDuringRegistrationEvent({ - firstStageMetadata, - programId, - orgUnitId, - currentEventValues, - fieldsValue, - attributeCategoryOptions, - }); - const autoGenerateEvents = deriveAutoGenerateEvents({ - stages, - enrolledAt, - occurredAt, - programId, - orgUnitId, - firstStageMetadata, - attributeCategoryOptions, - }); - const allEventsToBeCreated = firstStageDuringRegistrationEvent - ? [firstStageDuringRegistrationEvent, ...autoGenerateEvents] - : autoGenerateEvents; - const eventIndex = allEventsToBeCreated.findIndex( + const eventIndex = enrollmentPayload.enrollments[0]?.events.findIndex( eventsToBeCreated => eventsToBeCreated.programStage === stageWithOpenAfterEnrollment?.id, ); return saveNewTrackedEntityInstanceWithEnrollment({ candidateForRegistration: { trackedEntities: [ - { - geometry: deriveGeometryFromFormValues(currentFormData), - enrollments: [ - { - geometry: standardGeoJson(geometry), - occurredAt: convertFn(occurredAt, dataElementTypes.DATE), - enrolledAt: convertFn(enrolledAt, dataElementTypes.DATE), - program: programId, - orgUnit: orgUnitId, - attributes: deriveAttributesFromFormValues(formServerValues), - status: 'ACTIVE', - events: allEventsToBeCreated, - }, - ], - orgUnit: orgUnitId, - trackedEntityType: trackedEntityType.id, - ...(trackedEntity && { trackedEntity }), - }, + enrollmentPayload, ], }, redirectTo, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js index a38c30e4e5..e36a6e10e6 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveAutoGenerateEvents.js @@ -24,7 +24,7 @@ export const deriveAutoGenerateEvents = ({ occurredAt: string, programId: string, orgUnitId: string, - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, attributeCategoryOptions: { [categoryId: string]: string } | string, }) => { // in case we have a program that does not have an incident date (occurredAt), such as Malaria case diagnosis, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js index bf40f45135..2e4f952f92 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/deriveFirstStageDuringRegistrationEvent.js @@ -16,7 +16,7 @@ export const deriveFirstStageDuringRegistrationEvent = ({ fieldsValue, attributeCategoryOptions, }: { - firstStageMetadata: ProgramStage, + firstStageMetadata: ?ProgramStage, programId: string, orgUnitId: string, currentEventValues?: { [id: string]: any }, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js index 26bd03daad..23d9d1a610 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/helpers/getStageWithOpenAfterEnrollment.js @@ -12,7 +12,7 @@ export const PAGES = { // when the event will not be created redirect to enrollmentEventNew export const getStageWithOpenAfterEnrollment = ( stages: Map, - firstStageMetadata: ProgramStage, + useFirstStageDuringRegistration: boolean, shouldRedirect: boolean, ) => { const stagesArray = [...stages.values()]; @@ -22,8 +22,8 @@ export const getStageWithOpenAfterEnrollment = ( if (shouldRedirect && firstStageWithOpenAfterEnrollment) { // event will be created during first stage registration if ( - firstStageMetadata && - firstStageMetadata.id === firstStageWithOpenAfterEnrollment.id + useFirstStageDuringRegistration + && stagesArray[0].id === firstStageWithOpenAfterEnrollment.id ) { return PAGES.enrollmentEventEdit; } diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/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/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js index a7f1b1bea7..70f6bc76a2 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.component.js @@ -6,38 +6,41 @@ import { DATA_ENTRY_ID } from '../../registerTei.const'; import teiClasses from './trackedEntityInstance.module.css'; import { TeiRegistrationEntry } from '../../../../../DataEntries'; import type { Props } from './dataEntryTrackedEntityInstance.types'; +import { useCurrentOrgUnitInfo } from '../../../../../../hooks/useCurrentOrgUnitInfo'; const RelationshipTrackedEntityInstancePlain = - ({ - theme, - onSave, - teiRegistrationMetadata = {}, - duplicatesReviewPageSize, - renderDuplicatesDialogActions, - renderDuplicatesCardActions, - ExistingUniqueValueDialogActions, - }: Props) => { - const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; - const { trackedEntityType } = teiRegistrationMetadata || {}; - const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + ({ + theme, + onSave, + teiRegistrationMetadata = {}, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { id: orgUnitId } = useCurrentOrgUnitInfo(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); - return ( - // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching - - ); - }; + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; export const RelationshipTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js index a3ac0f0ae5..20f5463e75 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -3,16 +3,18 @@ import type { Node } from 'react'; import type { TeiRegistration } from '../../../../../../metaData'; import type { RenderCustomCardActions } from '../../../../../CardList'; import type { - SaveForEnrollmentAndTeiRegistration, ExistingUniqueValueDialogActionsComponent, } from '../../../../../DataEntries'; +import type { + TeiPayload, +} from '../../../../common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types'; export type Props = {| theme: Theme, - onSave: SaveForEnrollmentAndTeiRegistration, + onSave: (TeiPayload) => void, teiRegistrationMetadata?: TeiRegistration, duplicatesReviewPageSize: number, renderDuplicatesCardActions?: RenderCustomCardActions, - renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, |}; diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js index 991425a334..cf9de6695b 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/RegisterTei.container.js @@ -34,6 +34,7 @@ export const RegisterTei = ({ onLink, onSave, onGetUnsavedAttributeValues }: Own trackedEntityName={trackedEntityName} newRelationshipProgramId={newRelationshipProgramId} error={error} - />); + /> + ); }; diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js index b748347358..3cced06ccf 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/TeiRelationship/SearchResults/TeiRelationshipSearchResults.component.js @@ -13,6 +13,7 @@ import { SearchResultsHeader } from '../../../../SearchResultsHeader'; import { type SearchGroup } from '../../../../../metaData'; import { ResultsPageSizeContext } from '../../../shared-contexts'; import type { ListItem } from '../../../../CardList/CardList.types'; +import { convertClientValuesToServer } from '../../../../../converters/helpers/clientToServer'; const SearchResultsPager = withNavigation()(Pagination); @@ -77,7 +78,8 @@ class TeiRelationshipSearchResultsPlain extends React.Component { } onAddRelationship = (item) => { - this.props.onAddRelationship(item.id, item.values); + const serverValues = convertClientValuesToServer(item.values, this.props.searchGroup.searchForm); + this.props.onAddRelationship(item.id, serverValues); } renderResults = () => { diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js index fb48b2a555..6ee03b475f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData/useCommonEnrollmentDomainData.types.js @@ -1,5 +1,4 @@ // @flow - export type DataValue = { dataElement: string, value: string, @@ -49,6 +48,7 @@ export type AttributeValue = {| value: string, |}; + export type Output = {| error?: any, enrollment?: EnrollmentData, 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 new file mode 100644 index 0000000000..2dbf0c7a20 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.js @@ -0,0 +1,42 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core/styles'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import enrollmentClasses from './enrollment.module.css'; +import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryEnrollment.types'; + +const NewEnrollmentRelationshipPlain = + ({ + theme, + onSave, + programId, + orgUnitId, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + + return ( + i18n.t('Save new {{trackedEntityTypeName}} and link', { + trackedEntityTypeName, + interpolation: { escapeValue: false }, + })} + onSave={onSave} + duplicatesReviewPageSize={duplicatesReviewPageSize} + renderDuplicatesDialogActions={renderDuplicatesDialogActions} + renderDuplicatesCardActions={renderDuplicatesCardActions} + ExistingUniqueValueDialogActions={ExistingUniqueValueDialogActions} + /> + ); + }; + +export const NewEnrollmentRelationship = withTheme()(NewEnrollmentRelationshipPlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js new file mode 100644 index 0000000000..507e09e61b --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.container.js @@ -0,0 +1,23 @@ +// @flow +import { connect } from 'react-redux'; +import { makeEnrollmentMetadataSelector } from './enrollment.selectors'; +import { NewEnrollmentRelationship } from './DataEntryEnrollment.component'; + +const makeMapStateToProps = () => { + const enrollmentMetadataSelector = makeEnrollmentMetadataSelector(); + + const mapStateToProps = (state: ReduxState) => { + const enrollmentMetadata = enrollmentMetadataSelector(state); + + return { + enrollmentMetadata, + programId: state.newRelationshipRegisterTei.programId, + orgUnitId: state.newRelationshipRegisterTei.orgUnit.id, + }; + }; + // $FlowFixMe[not-an-object] automated comment + return mapStateToProps; +}; + +// $FlowFixMe +export const DataEntryEnrollment = connect(makeMapStateToProps, () => ({}))(NewEnrollmentRelationship); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js new file mode 100644 index 0000000000..de3f6cee6f --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/dataEntryEnrollment.types.js @@ -0,0 +1,20 @@ +// @flow +import type { Node } from 'react'; +import type { Enrollment } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + SaveForEnrollmentAndTeiRegistration, + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type Props = {| + theme: Theme, + programId: string, + orgUnitId: string, + enrollmentMetadata?: Enrollment, + onSave: SaveForEnrollmentAndTeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: SaveForEnrollmentAndTeiRegistration) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css new file mode 100644 index 0000000000..953ddeae59 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js new file mode 100644 index 0000000000..fd9331f6a6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/enrollment.selectors.js @@ -0,0 +1,23 @@ +// @flow +import { createSelector } from 'reselect'; +import type { TrackerProgram } from '../../../../../../../metaData'; +import { getProgramFromProgramIdThrowIfNotFound } from '../../../../../../../metaData'; + +const programIdSelector = state => state.newRelationshipRegisterTei.programId; + +// $FlowFixMe +export const makeEnrollmentMetadataSelector = () => createSelector( + programIdSelector, + (programId: string) => { + let program: TrackerProgram; + try { + // $FlowFixMe[incompatible-type] automated comment + program = getProgramFromProgramIdThrowIfNotFound(programId); + } catch (error) { + return null; + } + + // $FlowFixMe + return program.enrollment; + }, +); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js new file mode 100644 index 0000000000..0ecadd4fab --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryEnrollment } from './DataEntryEnrollment.container'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js new file mode 100644 index 0000000000..8cb9a9b5fa --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.actions.js @@ -0,0 +1,14 @@ +// @flow +import { actionCreator } from '../../../../../../actions/actions.utils'; + +export const actionTypes = { + DATA_ENTRY_OPEN: 'NewRelationshipRegisterTeiDataEntryOpen', + DATA_ENTRY_OPEN_CANCELLED: 'NewRelationshopRegisterTeiDataEntryOpenCancelled', + DATA_ENTRY_OPEN_FAILED: 'NewRelationshopRegisterTeiDataEntryOpenFailed', +}; + +export const openDataEntry = () => + actionCreator(actionTypes.DATA_ENTRY_OPEN)(); + +export const openDataEntryFailed = (errorMessage: string) => + actionCreator(actionTypes.DATA_ENTRY_OPEN_FAILED)({ errorMessage }); 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 new file mode 100644 index 0000000000..292a1d1530 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.component.js @@ -0,0 +1,37 @@ +// @flow +import * as React from 'react'; +import { DataEntryEnrollment } from './Enrollment'; +import { DataEntryTrackedEntityInstance } from './TrackedEntityInstance'; + +type Props = { + showDataEntry: boolean, + programId: string, + onSaveWithoutEnrollment: () => void, + onSaveWithEnrollment: () => void, +}; + +export class RegisterTeiDataEntryComponent extends React.Component { + render() { + const { showDataEntry, programId, onSaveWithoutEnrollment, onSaveWithEnrollment, ...passOnProps } = this.props; + + if (!showDataEntry) { + return null; + } + + if (programId) { + return ( + + ); + } + + return ( + + ); + } +} diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js new file mode 100644 index 0000000000..e5e7d50e76 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/RegisterTeiDataEntry.container.js @@ -0,0 +1,20 @@ +// @flow +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { RegisterTeiDataEntryComponent } from './RegisterTeiDataEntry.component'; +import { withErrorMessageHandler } from '../../../../../../HOC/withErrorMessageHandler'; + +const mapStateToProps = (state: ReduxState) => ({ + showDataEntry: state.newRelationshipRegisterTei.orgUnit, + error: state.newRelationshipRegisterTei.dataEntryError, + programId: state.newRelationshipRegisterTei.programId, +}); + +const mapDispatchToProps = () => ({}); + +export const RegisterTeiDataEntry = + compose( + // $FlowFixMe[missing-annot] automated comment + connect(mapStateToProps, mapDispatchToProps), + withErrorMessageHandler(), + )(RegisterTeiDataEntryComponent); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js new file mode 100644 index 0000000000..c455e3616d --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/DataEntryTrackedEntityInstance.js @@ -0,0 +1,52 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withTheme } from '@material-ui/core'; +import { DATA_ENTRY_ID } from '../../registerTei.const'; +import teiClasses from './trackedEntityInstance.module.css'; +import { TeiRegistrationEntry } from '../../../../../../DataEntries'; +import type { Props } from './dataEntryTrackedEntityInstance.types'; +import { getTeiRegistrationMetadata } from './tei.selectors'; +import { useLocationQuery } from '../../../../../../../utils/routing'; + +const RelationshipTrackedEntityInstancePlain = + ({ + theme, + onSave, + trackedEntityTypeId, + duplicatesReviewPageSize, + renderDuplicatesDialogActions, + renderDuplicatesCardActions, + ExistingUniqueValueDialogActions, + }: Props) => { + const { orgUnitId } = useLocationQuery(); + const fieldOptions = { theme, fieldLabelMediaBasedClass: teiClasses.fieldLabelMediaBased }; + const teiRegistrationMetadata = getTeiRegistrationMetadata(trackedEntityTypeId); + const { trackedEntityType } = teiRegistrationMetadata || {}; + const trackedEntityTypeNameLC = trackedEntityType.name.toLocaleLowerCase(); + + if (!teiRegistrationMetadata && !teiRegistrationMetadata?.form) { + return null; + } + + return ( + // $FlowFixMe - flow error will be resolved when rewriting relationship metadata fetching + + ); + }; + +export const DataEntryTrackedEntityInstance = withTheme()(RelationshipTrackedEntityInstancePlain); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js new file mode 100644 index 0000000000..b4f9617052 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/dataEntryTrackedEntityInstance.types.js @@ -0,0 +1,30 @@ +// @flow +import type { Node } from 'react'; +import type { TeiRegistration } from '../../../../../../../metaData'; +import type { RenderCustomCardActions } from '../../../../../../CardList'; +import type { + ExistingUniqueValueDialogActionsComponent, +} from '../../../../../../DataEntries'; + +export type TeiPayload = {| + trackedEntity: string, + trackedEntityType: string, + enrollments: [], + orgUnit: string, + geometry: ?{ coordinates: any, type: any }, + attributes: Array<{| + attribute: string, + value: any, + |}>, +|} + +export type Props = {| + theme: Theme, + trackedEntityTypeId: string, + onSave: TeiPayload => void, + teiRegistrationMetadata?: TeiRegistration, + duplicatesReviewPageSize: number, + renderDuplicatesCardActions?: RenderCustomCardActions, + renderDuplicatesDialogActions?: (onCancel: () => void, onSave: (TeiPayload) => void) => Node, + ExistingUniqueValueDialogActions: ExistingUniqueValueDialogActionsComponent, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js new file mode 100644 index 0000000000..5bb8975389 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/index.js @@ -0,0 +1,2 @@ +// @flow +export { DataEntryTrackedEntityInstance } from './DataEntryTrackedEntityInstance'; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js new file mode 100644 index 0000000000..2363e24584 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/tei.selectors.js @@ -0,0 +1,18 @@ +// @flow +import log from 'loglevel'; +import { errorCreator } from 'capture-core-utils'; +import type { TrackedEntityType } from '../../../../../../../metaData'; +import { getTrackedEntityTypeThrowIfNotFound } from '../../../../../../../metaData'; + +// $FlowFixMe +export const getTeiRegistrationMetadata = (TETypeId: string) => { + let TEType: TrackedEntityType; + try { + TEType = getTrackedEntityTypeThrowIfNotFound(TETypeId); + } catch (error) { + log.error(errorCreator('Could not get TrackedEntityType for id')({ TETypeId })); + return null; + } + + return TEType.teiRegistration; +}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css new file mode 100644 index 0000000000..8d899a1886 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/TrackedEntityInstance/trackedEntityInstance.module.css @@ -0,0 +1,11 @@ +@media screen and (max-width: 811px) and (min-width: 564px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} + +@media screen and (max-width: 451px) { + .fieldLabelMediaBased { + padding-top: 0px !important; + } +} \ No newline at end of file diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js new file mode 100644 index 0000000000..9a9a7070af --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.component.js @@ -0,0 +1,130 @@ +// @flow +import React, { type ComponentType, useContext, useCallback } from 'react'; +import { compose } from 'redux'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { Button } from '@dhis2/ui'; +import { RegisterTeiDataEntry } from './DataEntry/RegisterTeiDataEntry.container'; +import { RegistrationSection } from './RegistrationSection'; +import { DataEntryWidgetOutput } from '../../../../DataEntryWidgetOutput/DataEntryWidgetOutput.container'; +import { ResultsPageSizeContext } from '../../../shared-contexts'; +import type { ComponentProps } from './RegisterTei.types'; +import { withErrorMessageHandler } from '../../../../../HOC'; + +const getStyles = () => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + leftContainer: { + flexGrow: 10, + flexBasis: 0, + margin: 8, + }, +}); + +const CardListButton = (({ teiId, values, handleOnClick }) => ( + +)); + +const DialogButtons = ({ onCancel, onSave, trackedEntityName }) => ( + <> + +
+ +
+ +); + +const RegisterTeiPlain = ({ + dataEntryId, + onLink, + onSaveWithoutEnrollment, + onSaveWithEnrollment, + onGetUnsavedAttributeValues, + trackedEntityName, + trackedEntityTypeId, + selectedScopeId, + classes, +}: ComponentProps) => { + const { resultsPageSize } = useContext(ResultsPageSizeContext); + + const renderDuplicatesCardActions = useCallback(({ item }) => ( + + ), [onLink]); + + const renderDuplicatesDialogActions = useCallback((onCancel, onSaveArgument) => ( + + ), [trackedEntityName]); + + const ExistingUniqueValueDialogActions = useCallback(({ teiId, attributeValues }) => ( + + ), [onLink]); + + return ( +
+
+ + +
+ + + } + /> +
+ ); +}; + +export const RegisterTeiComponent: ComponentType<$Diff> = + compose( + withErrorMessageHandler(), + withStyles(getStyles), + )(RegisterTeiPlain); 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 new file mode 100644 index 0000000000..773d0c2778 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.container.js @@ -0,0 +1,34 @@ +// @flow +import React from 'react'; +import { useSelector } from 'react-redux'; +import { RegisterTeiComponent } from './RegisterTei.component'; +import type { ContainerProps } from './RegisterTei.types'; +import { useScopeInfo } from '../../../../../hooks'; + +export const RegisterTei = ({ + onLink, + onSave, + onGetUnsavedAttributeValues, + trackedEntityTypeId, + suggestedProgramId, +}: ContainerProps) => { + const dataEntryId = 'relationship'; + const error = useSelector(({ newRelationshipRegisterTei }) => (newRelationshipRegisterTei.error)); + const selectedScopeId = suggestedProgramId || trackedEntityTypeId; + const { trackedEntityName } = useScopeInfo(selectedScopeId); + + 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 new file mode 100644 index 0000000000..0de2b570ba --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegisterTei.types.js @@ -0,0 +1,23 @@ +// @flow +export type SharedProps = {| + onLink: (teiId: string, values: Object) => void, + onGetUnsavedAttributeValues?: ?Function, + trackedEntityTypeId: string, +|}; + +export type ContainerProps = {| + suggestedProgramId: string, + onSave: (teiPayload: Object) => void, + ...SharedProps, +|}; + +export type ComponentProps = {| + selectedScopeId: string, + error: string, + dataEntryId: string, + trackedEntityName: ?string, + onSaveWithEnrollment: () => void, + onSaveWithoutEnrollment: () => void, + ...SharedProps, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js new file mode 100644 index 0000000000..f815b36498 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/ProgramSelector/ComposedProgramSelector.component.js @@ -0,0 +1,157 @@ +// @flow +import * as React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import i18n from '@dhis2/d2-i18n'; +import { LinkButton } from '../../../../../../Buttons/LinkButton.component'; +import { ProgramFilterer } from '../../../../../../ProgramFilterer'; +import type { Program } from '../../../../../../../metaData'; +import { TrackerProgram } from '../../../../../../../metaData'; +import { + VirtualizedSelectField, + withSelectTranslations, + withFocusSaver, + withDefaultFieldContainer, + withLabel, + withFilterProps, +} from '../../../../../../FormFields/New'; +import { NonBundledDhis2Icon } from '../../../../../../NonBundledDhis2Icon'; + +const getStyles = (theme: Theme) => ({ + iconContainer: { + display: 'flex', + alignItems: 'center', + paddingRight: 5, + }, + icon: { + width: 22, + height: 22, + borderRadius: 2, + }, + isFilteredContainer: { + fontSize: 12, + color: theme.palette.grey.dark, + paddingTop: 5, + }, + isFilteredLink: { + paddingLeft: 2, + backgroundColor: 'inherit', + }, +}); + +type Option = { + label: string, + value: string, + iconLeft?: ?React.Node, +}; + +type Props = { + orgUnitIds: ?Array, + value: string, + trackedEntityTypeId: string, + classes: Object, + onUpdateSelectedProgram: (programId: string) => void, + onClearFilter: () => void, +}; + +class ProgramSelector extends React.Component { + baseLineFilter = (program: Program) => { + const { trackedEntityTypeId } = this.props; + + const isValid = program instanceof TrackerProgram && + program.trackedEntityType.id === trackedEntityTypeId && + program.access.data.write; + + return isValid; + } + + getOptionsFromPrograms = (programs: Array): Array