diff --git a/CHANGELOG.md b/CHANGELOG.md index ce30ef0f44..e3da265385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# [100.53.0](https://github.com/dhis2/capture-app/compare/v100.52.0...v100.53.0) (2024-01-26) + + +### Features + +* [DHIS2-15475] Form Field Plugins ([#3502](https://github.com/dhis2/capture-app/issues/3502)) ([2ccb87b](https://github.com/dhis2/capture-app/commit/2ccb87bb775056c701dfaf9b4f9494b0008a57c3)) + +# [100.52.0](https://github.com/dhis2/capture-app/compare/v100.51.0...v100.52.0) (2024-01-25) + + +### Features + +* [DHIS2-16204] make opt-in to new dashboard the default ([#3508](https://github.com/dhis2/capture-app/issues/3508)) ([5407d7c](https://github.com/dhis2/capture-app/commit/5407d7c98257e16cd647438a7cfe693a5fcd2e0e)) + +# [100.51.0](https://github.com/dhis2/capture-app/compare/v100.50.7...v100.51.0) (2024-01-25) + + +### Features + +* [DHIS2-15480] widget assignee ([#3412](https://github.com/dhis2/capture-app/issues/3412)) ([d61efae](https://github.com/dhis2/capture-app/commit/d61efae8b24ffb34a84e765bc397de87ce76c125)) + ## [100.50.7](https://github.com/dhis2/capture-app/compare/v100.50.6...v100.50.7) (2024-01-23) diff --git a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature index 966cd9083f..5964f07644 100644 --- a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature +++ b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm.feature @@ -47,9 +47,8 @@ Feature: User interacts with the Enrollment New Event Workspace Then the input should throw an error with error-message Please provide a positive integer Scenario: User should be asked to create new event after completing a stage and choose to cancel - Given you open the main page with Ngelehun and Malaria focus investigation context - And you opt in to use the new enrollment Dashboard for Malaria focus investigation - Then you land on the enrollment new event page by having typed #/enrollmentEventNew?enrollmentId=zRfAPUpjoG3&orgUnitId=DiszpKrYNg8&programId=M3xtLkYBlKI&stageId=CWaAcQYKVpq&teiId=S3JjTA4QMNe + Given you land on the enrollment new event page by having typed #/enrollmentEventNew?enrollmentId=zRfAPUpjoG3&orgUnitId=DiszpKrYNg8&programId=M3xtLkYBlKI&stageId=CWaAcQYKVpq&teiId=S3JjTA4QMNe + And the data store is clean Then you see the following Enrollment: New Event And you see the widget header Foci investigation & classification And you type 2022-01-01 in the input number 0 diff --git a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js index d4b2f403a9..4438466f15 100644 --- a/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js +++ b/cypress/e2e/EnrollmentAddEventPage/EnrollmentAddEventPageForm/index.js @@ -1,5 +1,6 @@ import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; +import '../../sharedSteps'; const showAllEventsInProgramStage = () => { cy.get('[data-test="dhis2-uicore-tablefoot"]') @@ -16,16 +17,6 @@ Given('you open the main page with Ngelehun and Malaria focus investigation cont cy.visit('/#/?orgUnitId=DiszpKrYNg8&programId=M3xtLkYBlKI'); }); -When(/^you opt in to use the new enrollment Dashboard for (.*)$/, (program) => { - cy.get('[data-test="main-page-working-list"]').then(($wrapper) => { - if ($wrapper.find('[data-test="opt-in"]').length > 0) { - cy.contains('[data-test="dhis2-uicore-button"]', `Opt in for ${program}`).click(); - cy.contains('[data-test="dhis2-uicore-button"]', 'Yes, opt in').click(); - cy.contains('[data-test="dhis2-uicore-button"]', `Opt out for ${program}`); - } - }); -}); - Given(/^you land on the enrollment new event page by having typed (.*)$/, (url) => { cy.visit(url); }); diff --git a/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature index ac67d53256..a00ec363c1 100644 --- a/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass.feature @@ -4,13 +4,12 @@ Feature: Breaking the glass page @skip Scenario: User with search scope access tries to access an enrollment in a protected program Given the tei created by this test is cleared from the database - And you opt temporarily in on new enrollment dashboard in Child programme and WHO RMNCH Tracker + And the data store is clean And you create a new tei in Child programme from Ngelehun CHC And you change program to WHO RMNCH Tracker And you enroll the tei from Njandama MCHP And you log out And you log in as tracker2 user - And you opt temporarily in on new enrollment dashboard in Child programme and WHO RMNCH Tracker And you select the new tei And you change program to WHO RMNCH Tracker Then you see the breaking the glass page diff --git a/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js b/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js index 120dc0a0dc..f8385c2fa7 100644 --- a/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js +++ b/cypress/e2e/EnrollmentPage/BreakingTheGlass/index.js @@ -1,5 +1,6 @@ import { Given, When, Then, defineStep as And } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; +import '../../sharedSteps'; Given('the tei created by this test is cleared from the database', () => { cy.buildApiUrl('tracker', 'trackedEntities?filter=w75KJ2mc4zz:like:Breaking&filter=zDhUuAYrxNC:like:TheGlass&trackedEntityType=nEenWmSyUEp&page=1&pageSize=5&ouMode=ACCESSIBLE') @@ -12,10 +13,6 @@ Given('the tei created by this test is cleared from the database', () => { )); }); -And('you opt temporarily in on new enrollment dashboard in Child programme and WHO RMNCH Tracker', () => { - cy.visit('/#/?newDashboard=IpHINAT79UW,WSGAb5XwJ3Y'); -}); - And('you create a new tei in Child programme from Ngelehun CHC', () => { cy.visit('/#/new?orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW'); cy.get('[data-test="capture-ui-input"]') diff --git a/cypress/e2e/MainPage.feature b/cypress/e2e/MainPage.feature index 980d9fd1e2..cb10b479fb 100644 --- a/cypress/e2e/MainPage.feature +++ b/cypress/e2e/MainPage.feature @@ -35,13 +35,14 @@ Feature: User interacts with Main page Then the current url is /#/?orgUnitId=DiszpKrYNg8&programId=uy2gU8kT1jF&selectedTemplateId=uy2gU8kT1jF-default And the TEI working list is displayed - Scenario: The admin user can optin to use the new Enrollment Dashboard + Scenario: The admin user can optout from using the new Enrollment Dashboard Given you open the main page with Ngelehun and child programme context - And you see the opt in component for Child Programme - When you opt in to use the new enrollment Dashboard for Child Programme - Then you see the opt out component for Child Programme + And the data store is clean + And you see the opt out component for Child Programme When you opt out to use the new enrollment Dashboard for Child Programme Then you see the opt in component for Child Programme + When you opt in to use the new enrollment Dashboard for Child Programme + Then you see the opt out component for Child Programme @v<41 Scenario: The icon is rendered as an svg diff --git a/cypress/e2e/NewPage.feature b/cypress/e2e/NewPage.feature index afbc13720c..7c814e0c4b 100644 --- a/cypress/e2e/NewPage.feature +++ b/cypress/e2e/NewPage.feature @@ -8,11 +8,12 @@ Feature: User creates a new entries from the registration page @v>=41 Scenario: New person in Tracker Program > Filling the Allergies with multiple options Given you are in the WHO RMNCH program registration page + And the data store is clean When you fill in multiple Allergies options Then you can see the multiple selections in the form And you fill the WHO RMNCH program registration form with its required unique values And you click the save person submit button - Then you are navigated to the WHO RMNCH program in Tracker Capture app + Then you see the enrollment event Edit page Scenario: Viewing the registration page with incomplete program categories selection Given you are in the main page with no selections made @@ -126,17 +127,19 @@ Feature: User creates a new entries from the registration page Scenario: New person > Submitting the form with unique name navigates you to the user dashboard Given you are in the Person registration page + And the data store is clean When you fill in a unique first name And you click the save person submit button - Then you are navigated to the Tracker Capture + Then you are navigated to the enrollment dashboard page without enrollment Scenario: New person > Submitting the form from the duplicates modal navigates you to the user dashboard Given you are in the Person registration page + And the data store is clean When you fill in the first name with value that has duplicates And you click the save person submit button And you see the possible duplicates modal And you submit the form again from the duplicates modal - Then you are navigated to the Tracker Capture + Then you are navigated to the enrollment dashboard page without enrollment Scenario: New person > Submitting the form shows a list with duplicates Given you are in the Person registration page @@ -171,17 +174,19 @@ Feature: User creates a new entries from the registration page Scenario: New person in Tracker Program > Submitting the form with unique values navigates you to the user dashboard Given you are in the WHO RMNCH program registration page + And the data store is clean When you fill the WHO RMNCH program registration form with its required unique values And you click the save person submit button - Then you are navigated to the WHO RMNCH program in Tracker Capture app + Then you see the enrollment event Edit page Scenario: New person in Tracker Program > Submitting the form from the duplicates modal navigates you to the user dashboard Given you are in the WHO RMNCH program registration page + And the data store is clean When you fill the WHO RMNCH program registration form with its required values And you click the save person submit button And you see the possible duplicates modal When you submit the form again from the duplicates modal - Then you are navigated to the WHO RMNCH program in Tracker Capture app + Then you see the enrollment event Edit page Scenario: New person in Tracker Program > Submitting the form shows a list with duplicates @@ -201,16 +206,11 @@ Feature: User creates a new entries from the registration page Then you see validation errors on the WHO RMNCH program registration page Scenario: Go to enrollment event when Open data entry form after enrollment is checked - Given you open the main page with Ngelehun and Malaria case diagnosis, treatment and investigation context - And you opt in to use the new enrollment Dashboard for Malaria case diagnosis, treatment and investigation - And you see the opt out component for Malaria case diagnosis, treatment and investigation - When you are in the Malaria case diagnosis, treatment and investigation program registration page + Given you are in the Malaria case diagnosis, treatment and investigation program registration page + And the data store is clean And you fill the Malaria case diagnosis registration form with values And you click the save malaria entity submit button Then you see the enrollment event Edit page - When you open the main page with Ngelehun and Malaria case diagnosis, treatment and investigation context - And you opt out to use the new enrollment Dashboard for Malaria case diagnosis, treatment and investigation - Then you see the opt in component for Malaria case diagnosis, treatment and investigation ## New enrollment of existing TEI diff --git a/cypress/e2e/NewPage/index.js b/cypress/e2e/NewPage/index.js index 901452f440..1bde1eb509 100644 --- a/cypress/e2e/NewPage/index.js +++ b/cypress/e2e/NewPage/index.js @@ -487,10 +487,6 @@ And('you fill in child programme first name with value that has duplicates', () .blur(); }); -Then('you are navigated to the WHO RMNCH program in Tracker Capture app', () => { - cy.url().should('include', 'dashboard?tei='); - cy.url().should('include', 'ou=DiszpKrYNg8&program=WSGAb5XwJ3Y'); -}); And('you fill the Child programme registration form with a first name with value that has duplicates', () => { cy.get('[data-test="capture-ui-input"]') diff --git a/cypress/e2e/ScopeSelector/index.js b/cypress/e2e/ScopeSelector/index.js index 3daaa87f6e..2a883d8298 100644 --- a/cypress/e2e/ScopeSelector/index.js +++ b/cypress/e2e/ScopeSelector/index.js @@ -294,7 +294,7 @@ And('you see the enrollment event Edit page but there is no org unit id in the u And('you see the enrollment event New page but there is no org unit id in the url', () => { cy.url().should('eq', `${Cypress.config().baseUrl}/#/enrollmentEventNew?enrollmentId=gPDueU02tn8&programId=IpHINAT79UW&stageId=A03MvHHogjR&teiId=fhFQhO0xILJ`); - cy.contains('Enrollment: New Event'); + cy.contains('Choose a registering unit to start reporting'); }); And('you see the enrollment event New page but there is no stage id in the url', () => { diff --git a/cypress/e2e/SearchPage.feature b/cypress/e2e/SearchPage.feature index 0f98fdfdfc..1c4591802f 100644 --- a/cypress/e2e/SearchPage.feature +++ b/cypress/e2e/SearchPage.feature @@ -24,10 +24,11 @@ Feature: User interacts with Search page Scenario: Searching using unique identifier returns results Given you are on the default search page + And the data store is clean When you select the search domain WHO RMNCH Tracker And you fill in the unique identifier field with values that will return a tracked entity instance And you click find - Then you are navigated to the Tracker Capture + Then you are navigated to the enrollment dashboard page # Scenario: Searching using attributes in Tracker Program returns no results # Given you are on the default search page @@ -96,23 +97,25 @@ Feature: User interacts with Search page Scenario: Searching using attributes in Tracker Program navigates user to the dashboard view Given you are on the default search page + And the data store is clean When you select the search domain WHO RMNCH Tracker And you expand the attributes search area And you fill in the last name with values that will return results And you click search And you can see the first page of the results And you click the view dashboard button - Then you are navigated to the Tracker Capture + Then you are navigated to the enrollment dashboard page Scenario: Searching using attributes in TEType navigates user to dashboard view Given you are on the default search page + And the data store is clean When you select the search domain Person And you expand the attributes search area And you fill in the the form with first name value: Cla And you click search And you can see the first page of the results And you click the view dashboard button - Then you are navigated to the Tracker Capture without program + Then you are navigated to the enrollment dashboard page without enrollment Scenario: Searching using attributes in Tracker Program domain has disabled pagination Given you are on the default search page @@ -162,9 +165,10 @@ Feature: User interacts with Search page Scenario: Pressing enter should trigger search unique identifier returns results Given you are on the default search page + And the data store is clean When you select the search domain WHO RMNCH Tracker And you press enter after filling in the unique identifier field with values that will return a tracked entity instance - Then you are navigated to the Tracker Capture + Then you are navigated to the enrollment dashboard page Scenario: Pressing enter should trigger search attributes returns results Given you are in the search page with the Child Programme being preselected from the url diff --git a/cypress/e2e/SearchPage/index.js b/cypress/e2e/SearchPage/index.js index 644ff73111..322dc9d377 100644 --- a/cypress/e2e/SearchPage/index.js +++ b/cypress/e2e/SearchPage/index.js @@ -93,19 +93,6 @@ When('you fill in the unique identifier field with values that will return a tra .blur(); }); -Then('you are navigated to the Tracker Capture', () => { - cy.url() - .should('include', 'dhis-web-tracker-capture/') - .should('include', 'dashboard?tei=') - .should('include', 'program=WSGAb5XwJ3Y'); -}); - -Then('you are navigated to the Tracker Capture without program', () => { - cy.url() - .should('include', 'dhis-web-tracker-capture/') - .should('include', 'dashboard?tei=') - .should('include', 'tracked_entity_type=nEenWmSyUEp'); -}); When('you fill in the first name with values that will return no results', () => { cy.get('[data-test="form-attributes"]') diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js new file mode 100644 index 0000000000..9f45c31e2f --- /dev/null +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetAssignee/index.js @@ -0,0 +1,52 @@ +import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; + +When('you assign the user Geetha in the view mode', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-assign"]').click(); + cy.get('[data-test="capture-ui-input"]').type('Geetha'); + cy.contains('Geetha Alwan').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +When('you assign the user Tracker demo User in the edit mode', () => { + cy + .get('[data-test="widget-enrollment-event"]') + .find('[data-test="dhis2-uicore-button"]') + .eq(1) + .click(); + + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-edit"]').click(); + cy.get('[data-test="dhis2-uicore-chip-remove"]').click(); + cy.get('[data-test="capture-ui-input"]').type('Tracker demo'); + cy.contains('Tracker demo User').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +When('you remove the assigned user', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-assignee-edit"]').click(); + cy.get('[data-test="dhis2-uicore-chip-remove"]').click(); + cy.get('[data-test="widget-assignee-save"]').click(); + }); +}); + +Then('the event has the user Geetha Alwan assigned', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('Geetha Alwan').should('exist'); + }); +}); + +Then('the event has the user Tracker demo User assigned', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('Tracker demo User').should('exist'); + }); +}); + +Then('the event has no assignd user', () => { + cy.get('[data-test="widget-assignee"]').within(() => { + cy.get('[data-test="widget-contents"]').contains('No one is assigned to this event').should('exist'); + }); +}); diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature index c3add0d727..27dae1fd6a 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent.feature @@ -102,4 +102,13 @@ Feature: The user interacts with the widgets on the enrollment edit event Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=XGLkLlOXgmE&orgUnitId=DiszpKrYNg8 Then the enrollment widget should be loaded When you click edit mode - Then list should contain the new comment: new test comment \ No newline at end of file + Then list should contain the new comment: new test comment + + Scenario: You can assign a user to a event + Given you land on the enrollment edit event page by having typed /#/enrollmentEventEdit?eventId=veuwiLC2x0e&orgUnitId=g8upMTyEZGZ + When you assign the user Geetha in the view mode + Then the event has the user Geetha Alwan assigned + When you assign the user Tracker demo User in the edit mode + Then the event has the user Tracker demo User assigned + When you remove the assigned user + Then the event has no assignd user diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js index 656f67c564..ff80131398 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentEditEvent/index.js @@ -2,3 +2,5 @@ import '../sharedSteps'; import '../WidgetEnrollment'; import '../WidgetProfile'; import '../WidgetEventComment'; +import '../WidgetAssignee'; + diff --git a/cypress/e2e/sharedSteps.js b/cypress/e2e/sharedSteps.js index 16967a0c21..6fa9febef3 100644 --- a/cypress/e2e/sharedSteps.js +++ b/cypress/e2e/sharedSteps.js @@ -155,12 +155,15 @@ Then(/^the user ?(.*) see the following text: (.*)$/, (not, message) => cy.contains(message).should(not ? 'not.exist' : 'exist'), ); -And('you navigated to the enrollment dashboard page', () => { - cy.url().should('include', 'enrollment?enrollmentId'); +And('you are navigated to the enrollment dashboard page', () => { + cy.url().should('include', 'enrollment?'); + cy.url().should('include', 'enrollmentId'); }); -And('you navigated to the enrollment dashboard page without enrollment', () => { - cy.url().should('include', 'enrollment?orgUnit'); +And('you are navigated to the enrollment dashboard page without enrollment', () => { + cy.url().should('include', 'enrollment?'); + cy.url().should('not.include', 'enrollmentId'); + cy.url().should('include', 'teiId'); }); Then('you should see no results found', () => { @@ -209,3 +212,9 @@ When(/^you opt out to use the new enrollment Dashboard for (.*)$/, (program) => Then(/^you see the opt in component for (.*)$/, (program) => { cy.contains('[data-test="dhis2-uicore-button"]', `Opt in for ${program}`); }); + +And('the data store is clean', () => { + cy.buildApiUrl('dataStore/capture/useNewDashboard') + .then(dataStoreUrl => + cy.request({ method: 'DELETE', url: dataStoreUrl, failOnStatusCode: false })); +}); diff --git a/i18n/en.pot b/i18n/en.pot index 0e54897d3d..2de5eeecc3 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -668,9 +668,6 @@ msgstr "There is an error while opening this enrollment. Please enter a valid ur msgid "An error occurred while fetching enrollments. Please enter a valid url." msgstr "An error occurred while fetching enrollments. Please enter a valid url." -msgid "Enrollment Dashboard" -msgstr "Enrollment Dashboard" - msgid "Quick actions" msgstr "Quick actions" @@ -769,9 +766,6 @@ msgstr "Schedule" msgid "Refer" msgstr "Refer" -msgid "Enrollment{{escape}} New Event" -msgstr "Enrollment{{escape}} New Event" - msgid "You can't add any more {{ programStageName }} events" msgstr "You can't add any more {{ programStageName }} events" @@ -787,9 +781,6 @@ msgstr "Program Stages could not be loaded" msgid "Stage" msgstr "Stage" -msgid "Enrollment{{escape}} Edit event" -msgstr "Enrollment{{escape}} Edit event" - msgid "Registered events" msgstr "Registered events" @@ -902,15 +893,6 @@ msgstr "" "Leaving this page will discard any selections you made for a new " "relationship" -msgid "No one is assigned to this event" -msgstr "No one is assigned to this event" - -msgid "Assign" -msgstr "Assign" - -msgid "Event assigned to {{name}}" -msgstr "Event assigned to {{name}}" - msgid "Feedbacks" msgstr "Feedbacks" @@ -926,6 +908,15 @@ msgstr "Event could not be loaded" msgid "Organisation unit could not be loaded" msgstr "Organisation unit could not be loaded" +msgid "Dashboard" +msgstr "Dashboard" + +msgid "Edit Event" +msgstr "Edit Event" + +msgid "View Event" +msgstr "View Event" + msgid "No feedback for this enrollment yet" msgstr "No feedback for this enrollment yet" @@ -1086,11 +1077,23 @@ msgstr "New {{trackedEntityName}} in {{programName}}" msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" -msgid "To work with the selected program," -msgstr "To work with the selected program," +msgid "Assigned to" +msgstr "Assigned to" + +msgid "You don't have access to edit this assignee" +msgstr "You don't have access to edit this assignee" -msgid "open the Tracker Capture app" -msgstr "open the Tracker Capture app" +msgid "Edit" +msgstr "Edit" + +msgid "No one is assigned to this event" +msgstr "No one is assigned to this event" + +msgid "You don't have access to assign an assignee" +msgstr "You don't have access to assign an assignee" + +msgid "Assign" +msgstr "Assign" msgid "This program is protected" msgstr "This program is protected" @@ -1167,9 +1170,6 @@ msgstr "Latitude" msgid "Longitude" msgstr "Longitude" -msgid "Edit" -msgstr "Edit" - msgid "Set coordinates" msgstr "Set coordinates" @@ -1365,9 +1365,6 @@ msgstr "This stage can only have one event" msgid "Events could not be retrieved. Please try again later." msgstr "Events could not be retrieved. Please try again later." -msgid "Assigned to" -msgstr "Assigned to" - msgid "{{ totalEvents }} events" msgstr "{{ totalEvents }} events" @@ -1563,6 +1560,9 @@ msgstr "Error deleting the enrollment event" msgid "Error editing the event, the changes made were not saved" msgstr "Error editing the event, the changes made were not saved" +msgid "Error updating the Assignee" +msgstr "Error updating the Assignee" + msgid "Set coordinate" msgstr "Set coordinate" diff --git a/package.json b/package.json index ae8d5f7c87..367d3990f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "capture-app", "homepage": ".", - "version": "100.50.7", + "version": "100.53.0", "cacheVersion": "7", "serverVersion": "38", "license": "BSD-3-Clause", @@ -10,7 +10,7 @@ "packages/rules-engine" ], "dependencies": { - "@dhis2/rules-engine-javascript": "100.50.7", + "@dhis2/rules-engine-javascript": "100.53.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 1b4e0c33de..871f0183d6 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,6 +1,6 @@ { "name": "@dhis2/rules-engine-javascript", - "version": "100.50.7", + "version": "100.53.0", "license": "BSD-3-Clause", "main": "./build/cjs/index.js", "scripts": { diff --git a/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions.js b/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions.js index bdbf975c72..d7cdfcba80 100644 --- a/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions.js +++ b/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions.js @@ -7,5 +7,10 @@ export const actionTypes = Object.freeze({ NAVIGATE_TO_ENROLLMENT_OVERVIEW: 'enrollmentNavigation.navigateToEnrollmentOverview', }); -export const navigateToEnrollmentOverview = ({ teiId, programId, orgUnitId, enrollmentId }: NavigateToEnrollmentOverviewProps) => +export const navigateToEnrollmentOverview = ({ + teiId, + programId, + orgUnitId, + enrollmentId, +}: NavigateToEnrollmentOverviewProps) => actionCreator(actionTypes.NAVIGATE_TO_ENROLLMENT_OVERVIEW)({ teiId, programId, orgUnitId, enrollmentId }); diff --git a/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.epics.js b/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.epics.js index f0f6f55506..32f7d066e2 100644 --- a/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.epics.js +++ b/src/core_modules/capture-core/actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.epics.js @@ -40,10 +40,15 @@ export const navigateToEnrollmentOverviewEpic = (action$: InputObservable, store switchMap((action) => { const { teiId, programId, orgUnitId } = action.payload; const enrollmentId = programId && (action.payload?.enrollmentId || 'AUTO'); - const { dataStore, userDataStore, temp } = store.value.useNewDashboard; + const { dataStore, userDataStore } = store.value.useNewDashboard; if (dataStore || userDataStore) { - const shouldRedirectToEnrollmentDashboard = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); + const shouldRedirectToEnrollmentDashboard = shouldUseNewDashboard({ + userDataStore, + dataStore, + programId, + teiId, + }); if (shouldRedirectToEnrollmentDashboard) { redirectToEnrollmentDashboard({ dependencies, teiId, programId, orgUnitId, enrollmentId }); return EMPTY; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js index d862669ae5..338d00e3ae 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/getConvertedNewSingleEvent.js @@ -72,7 +72,7 @@ export const getAddEventEnrollmentServerData = (state: ReduxState, program: programId, programStage: formFoundation.id, orgUnit: orgUnitId, - trackedEntityInstance: teiId, + trackedEntity: teiId, enrollment: enrollmentId, ...getApiCategoriesArgument(state.currentSelections.categories), dataValues: Object diff --git a/src/core_modules/capture-core/components/DataStore/DataStore.epics.js b/src/core_modules/capture-core/components/DataStore/DataStore.epics.js index 3462b63994..71c1cee90c 100644 --- a/src/core_modules/capture-core/components/DataStore/DataStore.epics.js +++ b/src/core_modules/capture-core/components/DataStore/DataStore.epics.js @@ -1,10 +1,25 @@ // @flow import { ofType } from 'redux-observable'; -import { mergeMap, catchError } from 'rxjs/operators'; +import { flatMap, catchError } from 'rxjs/operators'; import { EMPTY } from 'rxjs'; import { saveDataStore } from './DataStore.actions'; import { type UseNewDashboard } from './DataStore.types'; import { appStartActionTypes } from '../../../../components/AppStart'; +import { programCollection } from '../../metaDataMemoryStores'; + +const setNewDashboardByDefault = (key: string, dataStoreValues) => { + if (!dataStoreValues) { + return {}; + } + const programs = [...programCollection.keys()]; + const valuesWithDefault = programs.reduce((acc, program) => { + const dataStoreValue = dataStoreValues[program]; + acc[program] = dataStoreValue === undefined ? true : dataStoreValue; + return acc; + }, {}); + + return { [key]: valuesWithDefault }; +}; const getDataStoreFromApi = async querySingleResource => querySingleResource({ @@ -19,10 +34,16 @@ const getUserDataStoreFromApi = async querySingleResource => export const fetchDataStoreEpic = (action$: InputObservable, _: ReduxStore, { querySingleResource }: ApiUtils) => action$.pipe( ofType(appStartActionTypes.APP_LOAD_SUCESS), - mergeMap(async () => { - const apiDataStore: UseNewDashboard = await getDataStoreFromApi(querySingleResource); - // $FlowFixMe - return saveDataStore({ dataStore: apiDataStore }); + flatMap(async () => { + const apiDataStore: ?UseNewDashboard = await getDataStoreFromApi(querySingleResource) + .catch((error) => { + if (error.details.httpStatusCode === 404) { + return {}; + } + return undefined; + }); + + return saveDataStore(setNewDashboardByDefault('dataStore', apiDataStore)); }), catchError(() => EMPTY), ); @@ -30,10 +51,10 @@ export const fetchDataStoreEpic = (action$: InputObservable, _: ReduxStore, { qu export const fetchUserDataStoreEpic = (action$: InputObservable, _: ReduxStore, { querySingleResource }: ApiUtils) => action$.pipe( ofType(appStartActionTypes.APP_LOAD_SUCESS), - mergeMap(async () => { + flatMap(async () => { const apiUserDataStore: UseNewDashboard = await getUserDataStoreFromApi(querySingleResource); // $FlowFixMe - return saveDataStore({ userDataStore: apiUserDataStore }); + return saveDataStore(setNewDashboardByDefault('userDataStore', apiUserDataStore)); }), catchError(() => EMPTY), ); diff --git a/src/core_modules/capture-core/components/FormFields/UserField/index.js b/src/core_modules/capture-core/components/FormFields/UserField/index.js index e534c4387a..e9cb31b656 100644 --- a/src/core_modules/capture-core/components/FormFields/UserField/index.js +++ b/src/core_modules/capture-core/components/FormFields/UserField/index.js @@ -1,3 +1,4 @@ // @flow export { UserField } from './UserField.component'; +export { UserSearch } from './UserSearch.component'; export type { User as UserFormField } from './types'; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/DefaultPageLayout/DefaultPageLayout.constants.js b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/DefaultPageLayout/DefaultPageLayout.constants.js index dcb82cd9dc..ea7fa191a7 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/DefaultPageLayout/DefaultPageLayout.constants.js +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/DefaultPageLayout/DefaultPageLayout.constants.js @@ -1,5 +1,4 @@ // @flow -import i18n from '@dhis2/d2-i18n'; import { QuickActions, StagesAndEvents, @@ -8,19 +7,18 @@ import { WidgetTypes, } from '../../../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; import type { - DefaultPageLayoutConfig, + PageLayoutConfig, WidgetConfig, } from '../../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types'; export const WidgetsForEnrollmentPageDefault: $ReadOnly<{ [key: string]: WidgetConfig }> = Object.freeze({ QuickActions, StagesAndEvents, - Notes: EnrollmentComment, + EnrollmentComment, ...DefaultWidgetsForEnrollmentOverview, }); -export const DefaultPageLayout: DefaultPageLayoutConfig = Object.freeze({ - title: i18n.t('Enrollment Dashboard'), +export const DefaultPageLayout: PageLayoutConfig = Object.freeze({ leftColumn: [ { type: WidgetTypes.COMPONENT, 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 979c3574b6..a3940941cf 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 @@ -34,6 +34,9 @@ import { } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout'; import { DefaultPageLayout, WidgetsForEnrollmentPageDefault } from './DefaultPageLayout'; import { LoadingMaskForPage } from '../../../LoadingMasks'; +import { + EnrollmentPageKeys, +} from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; export const EnrollmentPageDefault = () => { @@ -132,6 +135,7 @@ export const EnrollmentPageDefault = () => { return ( , program: TrackerProgram, enrollmentId: string, teiId: string, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js index 60cd233099..206754c486 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.js @@ -6,6 +6,9 @@ import withStyles from '@material-ui/core/styles/withStyles'; import type { Props } from './EnrollmentAddEventPageDefault.types'; import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessage'; import { EnrollmentPageLayout } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; +import { + EnrollmentPageKeys, +} from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; const styles = ({ typography }) => ({ container: { @@ -76,6 +79,7 @@ const EnrollmentAddEventPagePain = ({
}
diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.js b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.js index 94ee19b337..6c869d96b6 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/PageLayout/DefaultPageLayout.constants.js @@ -1,7 +1,6 @@ // @flow -import i18n from '@dhis2/d2-i18n'; import type { - DefaultPageLayoutConfig, + PageLayoutConfig, WidgetConfig, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types'; import { @@ -15,8 +14,7 @@ export const WidgetsForEnrollmentEventNew: $ReadOnly<{ [key: string]: WidgetConf ...DefaultWidgetsForEnrollmentOverview, }); -export const DefaultPageLayout: DefaultPageLayoutConfig = Object.freeze({ - title: i18n.t('Enrollment{{escape}} New Event', { escape: ':' }), +export const DefaultPageLayout: PageLayoutConfig = Object.freeze({ leftColumn: [ { type: WidgetTypes.COMPONENT, diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js new file mode 100644 index 0000000000..ed817f7708 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.actions.js @@ -0,0 +1,15 @@ +// @flow + +import { actionCreator } from '../../../actions/actions.utils'; +import type { UserFormField } from '../../FormFields/UserField'; + +export const actionTypes = { + ASSIGNEE_SET: 'EnrollmentDomain.AssigneeSet', + ASSIGNEE_SAVE_FAILED: 'EnrollmentDomain.AssigneeSaveFailed', +}; + +export const setAssignee = (assignedUser?: ApiAssignedUser, assignee: UserFormField | null, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SET)({ assignedUser, assignee, eventId }); + +export const rollbackAssignee = (assignedUser?: ApiAssignedUser, assignee: UserFormField | null, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SAVE_FAILED)({ assignedUser, assignee, eventId }); 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 8ea5fe15a6..06f6f99b68 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 @@ -7,6 +7,9 @@ import { TopBar } from './TopBar.container'; import { NoticeBox } from '../../NoticeBox'; import { EnrollmentPageLayout } from '../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; import { WidgetsForEnrollmentEventEdit } from './PageLayout/DefaultPageLayout.constants'; +import { + EnrollmentPageKeys, +} from '../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; export const EnrollmentEditEventPageComponent = ({ pageLayout, @@ -29,11 +32,16 @@ export const EnrollmentEditEventPageComponent = ({ eventDate, scheduleDate, eventStatus, + eventAccess, + assignee, pageStatus, onEnrollmentError, onEnrollmentSuccess, onCancelEditEvent, onHandleScheduleSave, + getAssignedUserSaveContext, + onSaveAssignee, + onSaveAssigneeError, }: PlainProps) => ( 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 09936393df..c7b8f81f27 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 @@ -17,7 +17,7 @@ import { changeEventFromUrl } from '../ViewEvent/ViewEventComponent/viewEvent.ac import { buildEnrollmentsAsOptions } from '../../ScopeSelector'; import { convertDateWithTimeForView, convertValue } from '../../../converters/clientToView'; import { dataElementTypes } from '../../../metaData/DataElement'; -import { useEvent } from './hooks'; +import { useEvent, useAssignee, useAssignedUserSaveContext } from './hooks'; import type { Props } from './EnrollmentEditEventPage.types'; import { LoadingMaskForPage } from '../../LoadingMasks'; import { cleanUpDataEntry } from '../../DataEntry'; @@ -29,6 +29,9 @@ import { } from '../common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout'; import { DataStoreKeyByPage } from '../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; import { DefaultPageLayout } from './PageLayout/DefaultPageLayout.constants'; +import { getProgramEventAccess } from '../../../metaData'; +import { setAssignee, rollbackAssignee } from './EnrollmentEditEventPage.actions'; +import { convertClientToServer } from '../../../converters'; const getEventDate = (event) => { const eventDataConvertValue = convertDateWithTimeForView(event?.occurredAt || event?.scheduledAt); @@ -63,6 +66,8 @@ export const EnrollmentEditEventPage = () => { const { loading, event } = useEvent(eventId); const { program: programId, programStage: stageId, trackedEntity: teiId, enrollment: enrollmentId } = event; const { orgUnitId, eventId: urlEventId, initMode } = useLocationQuery(); + const enrollmentSite = useCommonEnrollmentDomainData(teiId, enrollmentId, programId).enrollment; + const storedEvent = enrollmentSite?.events?.find(item => item.event === eventId); useEffect(() => { if (!urlEventId) { @@ -73,16 +78,17 @@ export const EnrollmentEditEventPage = () => { } }, [dispatch, history, eventId, urlEventId, orgUnitId]); - return (!loading && eventId === urlEventId) || error ? ( + return ((!loading && eventId === urlEventId) || error) && storedEvent ? ( ) : ; }; @@ -93,8 +99,9 @@ const EnrollmentEditEventPageWithContextPlain = ({ teiId, enrollmentId, orgUnitId, - eventId, initMode, + enrollmentSite, + event, }: Props) => { const history = useHistory(); const dispatch = useDispatch(); @@ -103,7 +110,7 @@ const EnrollmentEditEventPageWithContextPlain = ({ dataStoreKey: DataStoreKeyByPage.ENROLLMENT_EVENT_EDIT, defaultPageLayout: DefaultPageLayout, }); - + const { event: eventId } = event; const { onLinkedRecordClick } = useLinkedRecordClick(); @@ -133,7 +140,6 @@ const EnrollmentEditEventPageWithContextPlain = ({ } }, [initMode, enrollmentId, eventId, orgUnitId, history]); - const { enrollment: enrollmentSite } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const onGoBack = () => history.push(`/enrollment?${buildUrlQueryString({ enrollmentId })}`); @@ -145,12 +151,12 @@ const EnrollmentEditEventPageWithContextPlain = ({ // $FlowFixMe 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); const scheduleDate = getEventScheduleDate(event); const { currentPageMode } = useEnrollmentEditEventPageMode(event?.status); const dataEntryKey = `${dataEntryIds.ENROLLMENT_EVENT}-${currentPageMode}`; const outputEffects = useWidgetDataFromStore(dataEntryKey); + const eventAccess = getProgramEventAccess(programId, programStage?.id); const pageStatus = getPageStatus({ @@ -162,6 +168,20 @@ const EnrollmentEditEventPageWithContextPlain = ({ event, isLoading, }); + const assignee = useAssignee(event); + const getAssignedUserSaveContext = useAssignedUserSaveContext(event); + const onSaveAssignee = (newAssignee) => { + // $FlowFixMe dataElementTypes flow error + const assignedUser: ApiAssignedUser = convertClientToServer(newAssignee, dataElementTypes.ASSIGNEE); + dispatch(setAssignee(assignedUser, newAssignee, eventId)); + }; + const onSaveAssigneeError = (prevAssignee) => { + const assignedUser: ApiAssignedUser | typeof undefined = prevAssignee + // $FlowFixMe dataElementTypes flow error + ? convertClientToServer(prevAssignee, dataElementTypes.ASSIGNEE) + : undefined; + dispatch(rollbackAssignee(assignedUser, prevAssignee, eventId)); + }; if (pageStatus === pageStatuses.LOADING) { return ; @@ -187,13 +207,18 @@ const EnrollmentEditEventPageWithContextPlain = ({ onAddNew={onAddNew} orgUnitId={orgUnitId} eventDate={eventDate} + assignee={assignee} onLinkedRecordClick={onLinkedRecordClick} onEnrollmentError={onEnrollmentError} onEnrollmentSuccess={onEnrollmentSuccess} eventStatus={event?.status} + eventAccess={eventAccess} scheduleDate={scheduleDate} onCancelEditEvent={onCancelEditEvent} onHandleScheduleSave={onHandleScheduleSave} + getAssignedUserSaveContext={getAssignedUserSaveContext} + onSaveAssignee={onSaveAssignee} + onSaveAssigneeError={onSaveAssigneeError} /> ); }; 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 38ebf0dcc5..3aa89694b7 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 { UserFormField } from '../../FormFields/UserField'; import type { LinkedRecordClick } from '../../WidgetsRelationship/WidgetTrackedEntityRelationship'; import type { PageLayoutConfig, @@ -33,6 +34,14 @@ export type PlainProps = {| onHandleScheduleSave: (eventData: Object) => void, pageStatus: string, eventStatus?: string, + eventAccess: {| + read: boolean, + write: boolean, + |} | null, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + assignee: UserFormField | null, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, |}; export type Props = {| @@ -41,6 +50,7 @@ export type Props = {| teiId: string, enrollmentId: string, orgUnitId: string, - eventId: string, + event: ApiEnrollmentEvent, + enrollmentSite: ApiEnrollment, initMode?: string, |}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js index f53de484e4..6a12ec64e7 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/PageLayout/DefaultPageLayout.constants.js @@ -1,24 +1,24 @@ // @flow -import i18n from '@dhis2/d2-i18n'; import type { - DefaultPageLayoutConfig, + PageLayoutConfig, WidgetConfig, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types'; import { DefaultWidgetsForEnrollmentOverview, EditEventWorkspace, EventComment, + AssigneeWidget, WidgetTypes, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout'; export const WidgetsForEnrollmentEventEdit: $ReadOnly<{ [key: string]: WidgetConfig }> = Object.freeze({ EditEventWorkspace, EventComment, + AssigneeWidget, ...DefaultWidgetsForEnrollmentOverview, }); -export const DefaultPageLayout: DefaultPageLayoutConfig = { - title: i18n.t('Enrollment{{escape}} Edit event', { escape: ':' }), +export const DefaultPageLayout: PageLayoutConfig = { leftColumn: [ { type: WidgetTypes.COMPONENT, @@ -26,6 +26,10 @@ export const DefaultPageLayout: DefaultPageLayoutConfig = { }, ], rightColumn: [ + { + type: WidgetTypes.COMPONENT, + name: 'AssigneeWidget', + }, { type: WidgetTypes.COMPONENT, name: 'ErrorWidget', diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js index 6ed8386853..c3906733fb 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/index.js @@ -1,2 +1,3 @@ // @flow export { useEvent } from './useEvent'; +export { useAssignee, useAssignedUserSaveContext } from './useAssignedUserSaveContext'; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js new file mode 100644 index 0000000000..3819ec28a6 --- /dev/null +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/hooks/useAssignedUserSaveContext.js @@ -0,0 +1,11 @@ +// @flow +import { useMemo, useCallback } from 'react'; +import { dataElementTypes } from '../../../../metaData'; +import { convertServerToClient } from '../../../../converters'; +import type { UserFormField } from '../../../FormFields/UserField'; + +export const useAssignee = (event: ApiEnrollmentEvent): UserFormField | null => + // $FlowFixMe dataElementTypes flow error + useMemo(() => convertServerToClient(event?.assignedUser, dataElementTypes.ASSIGNEE), [event?.assignedUser]); + +export const useAssignedUserSaveContext = (event: ApiEnrollmentEvent) => useCallback(() => ({ event }), [event]); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js index 3ce5e6a217..5a8c768ab0 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/index.js @@ -1,3 +1,6 @@ // @flow export { EnrollmentEditEventPage } from './EnrollmentEditEventPage.container'; export { updateEventSucceededEpic, updateEventFailedEpic } from './EnrollmentEditEventPage.epics'; +export { + actionTypes as enrollmentEditEventActionTypes, +} from './EnrollmentEditEventPage.actions'; diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js index 0f307053c7..976ce35e57 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPage.container.js @@ -7,7 +7,6 @@ import { programCollection } from 'capture-core/metaDataMemoryStores/programColl import { MainPageComponent } from './MainPage.component'; import { withLoadingIndicator } from '../../../HOC'; import { updateShowAccessibleStatus } from '../actions/crossPage.actions'; -import { enableNewDashboardsTemporarily } from '../../../utils/routing/newDashboard.actions'; import { buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { MainPageStatuses } from './MainPage.constants'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; @@ -87,7 +86,7 @@ const useCallbackMainPage = ({ orgUnitId, programId, showAllAccessible, history const MainPageContainer = () => { const dispatch = useDispatch(); const history = useHistory(); - const { all, programId, orgUnitId, selectedTemplateId, newDashboard } = useLocationQuery(); + const { all, programId, orgUnitId, selectedTemplateId } = useLocationQuery(); const showAllAccessible = all !== undefined; const { @@ -112,12 +111,6 @@ const MainPageContainer = () => { dispatch(updateShowAccessibleStatus(showAllAccessible)); }, [showAllAccessible, dispatch]); - useEffect(() => { - if (newDashboard) { - dispatch(enableNewDashboardsTemporarily(newDashboard.split(','))); - } - }, [dispatch, newDashboard]); - useEffect(() => { if (programId && trackedEntityTypeId && displayFrontPageList && selectedTemplateId === undefined) { handleChangeTemplateUrl({ 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 be02acbcb5..686fa6b24e 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 @@ -53,11 +53,11 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic: Epic = ( ofType(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_START), map((action) => { const { currentSelections: { programId } } = store.value; - const { dataStore, userDataStore, temp } = store.value.useNewDashboard; + const { dataStore, userDataStore } = store.value.useNewDashboard; const { enrollmentPayload, uid } = action.payload; const { stages, useFirstStageDuringRegistration } = getTrackerProgramThrowIfNotFound(programId); - const shouldRedirect = shouldUseNewDashboard(userDataStore, dataStore, temp, programId); + const shouldRedirect = shouldUseNewDashboard({ userDataStore, dataStore, programId }); const { stageWithOpenAfterEnrollment, redirectTo } = getStageWithOpenAfterEnrollment( stages, useFirstStageDuringRegistration, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js deleted file mode 100644 index dde335035b..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.component.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { IconUser24 } from '@dhis2/ui'; -import { ViewEventSection } from '../../Section/ViewEventSection.component'; -import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.component'; -import { Contents } from './Contents.component'; -import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; -import { type ProgramStage } from '../../../../../metaData'; - -const LoadingContents = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Contents); - -type Props = { - programStage: ProgramStage, - classes: Object, -} - -const loadingIndicatorStyle = { - height: 36, - width: 36, -}; - -export class AssigneeSectionComponent extends React.Component { - renderHeader = () => ( - - ) - - render() { - const { programStage, ...passOnProps } = this.props; - - if (!programStage.enableUserAssignment) { - return null; - } - - return ( - - {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - - - ); - } -} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js index b22b90aa22..4698089e83 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.js @@ -1,25 +1,35 @@ // @flow -import { connect } from 'react-redux'; -import { AssigneeSectionComponent } from './AssigneeSection.component'; -import { setAssignee } from './assigneeSection.actions'; +import React from 'react'; +import { WidgetAssignee } from '../../../../WidgetAssignee'; +import type { ProgramStage } from '../../../../../metaData'; +import type { UserFormField } from '../../../../FormFields/UserField'; -const mapStateToProps = (state: ReduxState) => { - const assigneeSection = state.viewEventPage.assigneeSection || {}; +type Props = {| + assignee: UserFormField | null, + programStage: ?ProgramStage, + eventAccess: {| + read: boolean, + write: boolean, + |} | null, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, +|}; - return { - assignee: (!assigneeSection.isLoading) ? - state.viewEventPage.loadedValues.eventContainer.event.assignee : - undefined, - ready: !assigneeSection.isLoading, - }; -}; - -const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ - onSet: (user: Object) => { - dispatch(setAssignee(user)); - }, -}); - -// $FlowSuppress -// $FlowFixMe[missing-annot] automated comment -export const AssigneeSection = connect(mapStateToProps, mapDispatchToProps)(AssigneeSectionComponent); +export const AssigneeSection = ({ + assignee, + programStage, + getAssignedUserSaveContext, + eventAccess, + onSaveAssignee, + onSaveAssigneeError, +}: Props) => ( + +); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js deleted file mode 100644 index 5d5daf12aa..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/Contents.component.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import * as React from 'react'; -import { DisplayMode } from './DisplayMode.component'; -import { EditMode } from './EditMode.component'; - -type Props = { - onSet: (user: Object) => void, -}; - -export const Contents = (props: Props) => { - const { onSet, ...passOnProps } = props; - const [editMode, setEditMode] = React.useState(false); - - const handleSet = React.useCallback((user) => { - setEditMode(false); - onSet(user); - }, [onSet]); - - const handleCancelSearch = React.useCallback(() => { - setEditMode(false); - }, []); - - if (editMode) { - return ( - - ); - } - - return ( - // $FlowFixMe[cannot-spread-inexact] automated comment - { setEditMode(true); }} - {...passOnProps} - /> - ); -}; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js deleted file mode 100644 index 394275ea37..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/DisplayMode.component.js +++ /dev/null @@ -1,90 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { withStyles, IconButton } from '@material-ui/core'; -import { IconEdit24, Button } from '@dhis2/ui'; - -const getStyles = () => ({ - container: { - display: 'flex', - alignItems: 'center', - }, - nameContainer: { - paddingRight: 5, - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - iconContainer: { - width: 24, - }, - editButton: { - color: 'inherit', - }, - addIcon: { - paddingRight: 5, - }, -}); - -type User = { - id: string, - username: string, - name: string, -}; - -type Props = { - assignee: ?User, - onEdit: () => void, - classes: Object, - eventAccess: { read: boolean, write: boolean }, -}; - -const DisplayModePlain = (props: Props) => { - const { eventAccess, assignee, onEdit, classes } = props; - - if (!assignee) { - if (!eventAccess.write) { - return ( -
- {i18n.t('No one is assigned to this event')} -
- ); - } - return ( -
- -
- ); - } - - return ( -
-
- {i18n.t('Event assigned to {{name}}', { name: assignee.name })} -
-
- {eventAccess.write ? - ( - - - - ) : null} -
-
- ); -}; - -export const DisplayMode = withStyles(getStyles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js deleted file mode 100644 index 41de1ddd9a..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/EditMode.component.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { Button } from '@dhis2/ui'; -import { withStyles } from '@material-ui/core/styles'; -import { UserSearch } from '../../../../FormFields/UserField/UserSearch.component'; - -const getStyles = () => ({ - container: { - display: 'flex', - alignItems: 'center', - }, - searchContainer: { - flexGrow: 1, - flexShrink: 1, - paddingRight: 5, - }, - buttonContainer: { - flexGrow: 0, - flexShrink: 0, - }, -}); - -type Props = { - onCancel: Function, - classes: Object, -}; - -const EditModePlain = (props: Props) => { - const { onCancel, classes, ...passOnProps } = props; - return ( -
-
- {/* $FlowFixMe[cannot-spread-inexact] automated comment */} - -
-
- -
-
- ); -}; - -export const EditMode = withStyles(getStyles)(EditModePlain); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js deleted file mode 100644 index 0bfd4a1db2..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/assigneeSection.actions.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -import { actionCreator } from '../../../../../actions/actions.utils'; -import { effectMethods } from '../../../../../trackerOffline'; - -export const actionTypes = { - VIEW_EVENT_ASSIGNEE_SET: 'ViewEventAssigneeSet', - VIEW_EVENT_ASSIGNEE_SAVE: 'ViewEventAssigneeSave', - VIEW_EVENT_ASSIGNEE_SAVE_COMPLETED: 'ViewEventAssigneeSaveCompleted', - VIEW_EVENT_ASSIGNEE_SAVE_FAILED: 'ViewEventAssigneeSaveFailed', -}; - -export const setAssignee = (assignee: Object) => - actionCreator(actionTypes.VIEW_EVENT_ASSIGNEE_SET)({ assignee }); - -export const saveAssignee = (eventId: string, serverData: Object, selections: Object) => - actionCreator(actionTypes.VIEW_EVENT_ASSIGNEE_SAVE)({}, { - offline: { - effect: { - url: 'tracker?async=false&importStrategy=UPDATE', - method: effectMethods.POST, - data: serverData, - }, - commit: { type: actionTypes.VIEW_EVENT_ASSIGNEE_SAVE_COMPLETED, meta: { eventId, selections } }, - rollback: { type: actionTypes.VIEW_EVENT_ASSIGNEE_SAVE_FAILED, meta: { eventId, selections } }, - }, - }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js index 5abb01a6c0..9646d16bb6 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/index.js @@ -1,4 +1,2 @@ // @flow -export { actionTypes as assigneeSectionActionTypes } from './assigneeSection.actions'; export { AssigneeSection } from './AssigneeSection.container'; -export { saveAssigneeEpic } from './saveAssignee.epic'; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js deleted file mode 100644 index 99f4a3348b..0000000000 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/saveAssignee.epic.js +++ /dev/null @@ -1,37 +0,0 @@ -// @flow -import { ofType } from 'redux-observable'; -import { map } from 'rxjs/operators'; -import { actionTypes, saveAssignee } from './assigneeSection.actions'; -import { getEventProgramThrowIfNotFound } from '../../../../../metaData'; -import { convertValue as convertToServerValue } from '../../../../../converters/clientToServer'; -import { convertMainEventClientToServer } from '../../../../../events/mainConverters'; - -export const saveAssigneeEpic = (action$: InputObservable, store: ReduxStore) => - action$.pipe( - ofType(actionTypes.VIEW_EVENT_ASSIGNEE_SET), - map(() => { - const state = store.value; - const eventId = state.viewEventPage.eventId; - const eventContainer = state.viewEventPage.loadedValues.eventContainer; - const { event: clientMainValues, values: clientValues } = eventContainer; - const program = getEventProgramThrowIfNotFound(clientMainValues.programId); - const formFoundation = program.stage.stageForm; - const formServerValues = formFoundation.convertValues(clientValues, convertToServerValue); - const mainDataServerValues: Object = convertMainEventClientToServer(clientMainValues); - - const serverData = { - events: [{ - ...mainDataServerValues, - dataValues: Object - .keys(formServerValues) - .map(key => ({ - dataElement: key, - value: formServerValues[key], - })), - }], - }; - - const currentSelectionSet = state.currentSelections; - - return saveAssignee(eventId, serverData, currentSelectionSet); - })); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/ConnectedEntity/TrackedEntityInstance.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/ConnectedEntity/TrackedEntityInstance.js index c4472cf677..e02de9b1e9 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/ConnectedEntity/TrackedEntityInstance.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/ConnectedEntity/TrackedEntityInstance.js @@ -15,11 +15,16 @@ type Props = { export const TrackedEntityInstance = ({ name, id, orgUnitId, linkProgramId }: Props) => { const { baseUrl } = useConfig(); - const { dataStore, userDataStore, temp } = useSelector(({ useNewDashboard }) => useNewDashboard); + const { dataStore, userDataStore } = useSelector(({ useNewDashboard }) => useNewDashboard); const getUrl = useCallback(() => { - if (linkProgramId && shouldUseNewDashboard(userDataStore, dataStore, temp, linkProgramId)) { - return `/#/enrollment?${buildUrlQueryString({ teiId: id, programId: linkProgramId, orgUnitId })}`; + if (shouldUseNewDashboard({ userDataStore, dataStore, programId: linkProgramId, teiId: id })) { + return `/#/enrollment?${buildUrlQueryString({ + teiId: id, + programId: linkProgramId, + orgUnitId, + enrollmentId: 'AUTO', + })}`; } const trackerBaseUrl = buildUrl(baseUrl, systemSettingsStore.get().trackerAppRelativePath, '/#/dashboard?'); const baseParams = `tei=${id}&ou=${orgUnitId}`; @@ -32,7 +37,6 @@ export const TrackedEntityInstance = ({ name, id, orgUnitId, linkProgramId }: Pr linkProgramId, dataStore, userDataStore, - temp, ]); return ( diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js index 307d8ccd30..d04213a437 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RightColumnWrapper.component.js @@ -9,7 +9,7 @@ import { FeedbacksSection } from './FeedbacksSection/FeedbacksSection.container' import { IndicatorsSection } from './IndicatorsSection/IndicatorsSection.container'; import { RelationshipsSection } from './RelationshipsSection/RelationshipsSection.container'; import { NotesSection } from './NotesSection/NotesSection.container'; -import { AssigneeSection } from './AssigneeSection/AssigneeSection.container'; +import { AssigneeSection } from './AssigneeSection'; type Props = { classes: { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js index dbb2406dff..de6353d5ea 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.component.js @@ -8,7 +8,7 @@ import { RightColumnWrapper } from '../RightColumn/RightColumnWrapper.component' import type { ProgramStage } from '../../../../metaData'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { defaultDialogProps } from '../../../Dialogs/DiscardDialog.constants'; - +import type { UserFormField } from '../../../FormFields/UserField'; const getStyles = (theme: Theme) => ({ container: { @@ -48,6 +48,10 @@ type Props = { header: string, showAllEvents: string, }, + assignee: UserFormField, + getAssignedUserSaveContext: () => { event: ApiEnrollmentEvent }, + onSaveAssignee: (newAssignee: UserFormField) => void, + onSaveAssigneeError: (prevAssignee: UserFormField | null) => void, }; type State = { @@ -70,7 +74,17 @@ class ViewEventPlain extends Component { } render() { - const { classes, programStage, currentDataEntryKey, eventAccess } = this.props; + const { + classes, + programStage, + currentDataEntryKey, + eventAccess, + assignee, + getAssignedUserSaveContext, + onSaveAssignee, + onSaveAssigneeError, + } = this.props; + return (
{ const programStageSelector = makeProgramStageSelector(); const eventAccessSelector = makeEventAccessSelector(); + const assignedUserContextSelector = makeAssignedUserContextSelector(); // $FlowFixMe[not-an-object] automated comment return (state: ReduxState) => { @@ -29,6 +31,9 @@ const makeMapStateToProps = () => { error: state.viewEventPage.loadError, currentDataEntryKey, isUserInteractionInProgress, + assignee: state.viewEventPage.loadedValues?.eventContainer.event.assignee, + getAssignedUserSaveContext: () => assignedUserContextSelector(state), + eventId: state.viewEventPage.eventId, }; }; }; @@ -37,10 +42,26 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({ onBackToAllEvents: () => { dispatch(startGoBackToMainPage()); }, + dispatch, }); +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const mergedProps = { + onSaveAssignee: (newAssignee) => { + dispatchProps.dispatch(setAssignee(newAssignee, stateProps.eventId)); + }, + onSaveAssigneeError: (prevAssignee) => { + dispatchProps.dispatch(rollbackAssignee(prevAssignee, stateProps.eventId)); + }, + }; + + return Object.assign({}, ownProps, stateProps, dispatchProps, mergedProps); +}; + // $FlowSuppress // $FlowFixMe[missing-annot] automated comment -export const ViewEvent = connect(makeMapStateToProps, mapDispatchToProps)( - withErrorMessageHandler()(ViewEventComponent), -); +export const ViewEvent = connect( + makeMapStateToProps, + mapDispatchToProps, + mergeProps, +)(withErrorMessageHandler()(ViewEventComponent)); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js index 2c4c627067..14413d97bc 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.actions.js @@ -1,6 +1,7 @@ // @flow import { actionCreator } from 'capture-core/actions/actions.utils'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; +import type { UserFormField } from '../../../FormFields/UserField'; export const actionTypes = { VIEW_EVENT_FROM_URL: 'ViewEventFromUrl', @@ -19,6 +20,8 @@ export const actionTypes = { UPDATE_WORKING_LIST_PENDING_ON_BACK_TO_MAIN_PAGE: 'UpdateWorkingListPendingOnBackToMainPageForViewEvent', OPEN_VIEW_EVENT_PAGE_FAILED: 'OpenViewEventPageFailed', INITIALIZE_WORKING_LISTS_ON_BACK_TO_MAIN_PAGE: 'InitializeWorkingListsOnBackToMainPage', + ASSIGNEE_SET: 'SingleEvent.AssigneeSet', + ASSIGNEE_SAVE_FAILED: 'SingleEvent.AssigneeSaveFailed', }; export const viewEventFromUrl = (data: Object) => @@ -74,3 +77,10 @@ export const updateEventContainer = (eventContainer: Object, orgUnit: OrgUnit) = export const openViewEventPageFailed = (error: string) => actionCreator(actionTypes.OPEN_VIEW_EVENT_PAGE_FAILED)({ error }); + +export const setAssignee = (assignee: UserFormField, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SET)({ assignee, eventId }); + +export const rollbackAssignee = (assignee: UserFormField, eventId: string) => + actionCreator(actionTypes.ASSIGNEE_SAVE_FAILED)({ assignee, eventId }); + diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js index 69cd29f1f6..afb7761610 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/viewEvent.selectors.js @@ -2,10 +2,12 @@ import { createSelector } from 'reselect'; import { getEventProgramEventAccess, getEventProgramThrowIfNotFound } from '../../../../metaData'; - +import { convertValue as convertToServerValue } from '../../../../converters/clientToServer'; +import { convertMainEventClientToServer } from '../../../../events/mainConverters'; const programIdSelector = state => state.currentSelections.programId; const categoriesMetaSelector = state => state.currentSelections.categoriesMeta; +const eventContainerSelector = state => state.viewEventPage.loadedValues?.eventContainer; // $FlowFixMe[missing-annot] automated comment export const makeProgramStageSelector = () => createSelector( @@ -18,3 +20,23 @@ export const makeEventAccessSelector = () => createSelector( categoriesMetaSelector, (programId: string, categoriesMeta: ?Object) => getEventProgramEventAccess(programId, categoriesMeta)); +export const makeAssignedUserContextSelector = () => + // $FlowFixMe[missing-annot] + createSelector(eventContainerSelector, (eventContainer) => { + const { event: clientMainValues, values: clientValues } = eventContainer; + const program = getEventProgramThrowIfNotFound(clientMainValues.programId); + const formFoundation = program.stage.stageForm; + const formServerValues = formFoundation.convertValues(clientValues, convertToServerValue); + const mainDataServerValues: Object = convertMainEventClientToServer(clientMainValues); + + const event = + { + ...mainDataServerValues, + dataValues: Object.keys(formServerValues).map(key => ({ + dataElement: key, + value: formServerValues[key], + })), + }; + + return { event }; + }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/index.js b/src/core_modules/capture-core/components/Pages/ViewEvent/index.js index 681239203a..082ff0bd70 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/index.js +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/index.js @@ -1,5 +1,3 @@ // @flow export { actionTypes as editEventDataEntryActionTypes } from '../../WidgetEventEdit/EditEventDataEntry'; -export { assigneeSectionActionTypes } from '../ViewEvent/RightColumn/AssigneeSection'; - export { ViewEventPage } from './ViewEventPage.container'; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants.js index f2d53eabb2..b06c213045 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants.js @@ -1,5 +1,5 @@ // @flow - +import i18n from '@dhis2/d2-i18n'; import { EnrollmentWidget, ErrorWidget, @@ -10,6 +10,20 @@ import { WarningWidget, } from './LayoutComponentConfig'; +export const EnrollmentPageKeys = Object.freeze({ + OVERVIEW: 'overview', + NEW_EVENT: 'newEvent', + EDIT_EVENT: 'editEvent', + VIEW_EVENT: 'viewEvent', +}); + +export const DefaultPageTitle = { + OVERVIEW: i18n.t('Dashboard'), + NEW_EVENT: i18n.t('New Event'), + EDIT_EVENT: i18n.t('Edit Event'), + VIEW_EVENT: i18n.t('View Event'), +}; + // Default components are available across all Enrollment Pages export const DefaultWidgetsForEnrollmentOverview = { TrackedEntityRelationship, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js index 3e1ebf0af4..50c819aeec 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.types.js @@ -5,6 +5,7 @@ import { WidgetTypes } from './DefaultEnrollmentLayout.constants'; type DefaultComponents = 'QuickActions' | 'StagesAndEvents' + | 'AssigneeWidget' | 'NewEventWorkspace' | 'EditEventWorkspace' | 'EnrollmentComment' @@ -31,13 +32,11 @@ export type PluginWidgetColumnConfig = { export type ColumnConfig = DefaultWidgetColumnComfig | PluginWidgetColumnConfig; export type PageLayoutConfig = { - title: ?string, + title?: ?string, leftColumn: ?Array, rightColumn: ?Array, } -export type DefaultPageLayoutConfig = $Exact; - export type WidgetConfig = { Component: React$ComponentType, shouldHideWidget?: (props: Object) => boolean, diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.js index 43ed4a3cca..2b47e27789 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/EnrollmentPageLayout.js @@ -1,10 +1,12 @@ // @flow import React, { useCallback, useMemo, useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; import { colors, spacers, spacersNum } from '@dhis2/ui'; import { withStyles } from '@material-ui/core/styles'; import { useWidgetColumns } from './hooks/useWidgetColumns'; import { AddRelationshipRefWrapper } from './AddRelationshipRefWrapper'; import type { PlainProps } from '../../../Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.types'; +import { DefaultPageTitle, EnrollmentPageKeys } from './DefaultEnrollmentLayout.constants'; const getEnrollmentPageStyles = () => ({ container: { @@ -42,9 +44,21 @@ const getEnrollmentPageStyles = () => ({ }, }); +const getTitle = (inputTitle, page) => { + const title = inputTitle || i18n.t('Enrollment'); + const titles = { + [EnrollmentPageKeys.OVERVIEW]: !inputTitle ? `${title} ${DefaultPageTitle.OVERVIEW}` : title, + [EnrollmentPageKeys.NEW_EVENT]: `${title}: ${DefaultPageTitle.NEW_EVENT}`, + [EnrollmentPageKeys.EDIT_EVENT]: `${title}: ${DefaultPageTitle.EDIT_EVENT}`, + [EnrollmentPageKeys.VIEW_EVENT]: `${title}: ${DefaultPageTitle.VIEW_EVENT}`, + }; + return titles[page] || title; +}; + const EnrollmentPageLayoutPlain = ({ pageLayout, availableWidgets, + currentPage, classes, ...passOnProps }: PlainProps) => { @@ -75,7 +89,7 @@ const EnrollmentPageLayoutPlain = ({ className={classes.contentContainer} style={!mainContentVisible ? { display: 'none' } : undefined} > -
{pageLayout.title}
+
{getTitle(pageLayout.title, currentPage)}
{pageLayout.leftColumn && !!leftColumnWidgets?.length && (
diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js index b471c2e6cd..a7de4addff 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.js @@ -25,6 +25,7 @@ import type { WidgetConfig } from '../DefaultEnrollmentLayout.types'; import { NewEventWorkspaceWrapper } from '../../../NewEventWorkspaceWrapper'; import { WidgetEventEditWrapper } from '../../../WidgetEventEditWrapper'; import { WidgetEventComment } from '../../../../../WidgetEventComment'; +import { WidgetAssignee } from '../../../../../WidgetAssignee'; export const QuickActions: WidgetConfig = { Component: EnrollmentQuickActions, @@ -182,7 +183,8 @@ export const EditEventWorkspace: WidgetConfig = { eventStatus, onCancelEditEvent, onHandleScheduleSave, - scheduleDate, + initialScheduleDate, + assignee, }) => ({ programStage, onGoBack, @@ -193,7 +195,27 @@ export const EditEventWorkspace: WidgetConfig = { eventStatus, onCancelEditEvent, onHandleScheduleSave, - initialScheduleDate: scheduleDate, + initialScheduleDate, + assignee, + }), +}; + +export const AssigneeWidget: WidgetConfig = { + Component: WidgetAssignee, + getProps: ({ + programStage, + assignee, + getAssignedUserSaveContext, + eventAccess, + onSaveAssignee, + onSaveAssigneeError, + }) => ({ + enabled: programStage?.enableUserAssignment || false, + assignee, + getSaveContext: getAssignedUserSaveContext, + writeAccess: eventAccess?.write || false, + onSave: onSaveAssignee, + onSaveError: onSaveAssigneeError, }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout.js index d5a535b8ae..26b6b3d02d 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/hooks/useEnrollmentPageLayout.js @@ -1,10 +1,10 @@ // @flow import { useApiMetadataQuery } from '../../../../../../utils/reactQueryHelpers'; -import type { DefaultPageLayoutConfig, PageLayoutConfig } from '../DefaultEnrollmentLayout.types'; +import type { PageLayoutConfig } from '../DefaultEnrollmentLayout.types'; type Props = { selectedScopeId: ?string, - defaultPageLayout: DefaultPageLayoutConfig, + defaultPageLayout: PageLayoutConfig, dataStoreKey: string, } export const useEnrollmentPageLayout = ({ selectedScopeId, defaultPageLayout, dataStoreKey }: Props) => { 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 688e293955..6a4a067870 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 @@ -17,9 +17,11 @@ export type Event = {| program: string, programStage: string, status: 'ACTIVE' | 'VISITED' | 'COMPLETED' | 'SCHEDULE' | 'OVERDUE' | 'SKIPPED', - trackedEntityInstance: string, + trackedEntity: string, notes?: Array, pendingApiResponse?: ?boolean, + assignedUser?: ApiAssignedUser, + followUp?: boolean, |}; export type EnrollmentData = {| diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js index d8ff56e1b7..f1d24d1b6f 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useRuleEffects/useRuleEffects.js @@ -33,7 +33,7 @@ const useEventsData = (enrollment, program) => { programId: event.program, programStageId: event.programStage, orgUnitId: event.orgUnit, - trackedEntityInstanceId: event.trackedEntityInstance, + trackedEntityInstanceId: event.trackedEntity, enrollmentId: event.enrollment, enrollmentStatus: event.enrollmentStatus, status: event.status, diff --git a/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.component.js b/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.component.js deleted file mode 100644 index 9df786c6e0..0000000000 --- a/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.component.js +++ /dev/null @@ -1,77 +0,0 @@ -// @flow -import React, { useCallback } from 'react'; -import { withStyles } from '@material-ui/core/styles'; -import Paper from '@material-ui/core/Paper'; -import { useConfig } from '@dhis2/app-runtime'; -import i18n from '@dhis2/d2-i18n'; -import { buildUrl } from 'capture-core-utils'; -import { getProgramFromProgramIdThrowIfNotFound, TrackerProgram } from '../../metaData'; -import { systemSettingsStore } from '../../metaDataMemoryStores'; - -const getStyles = () => ({ - container: { - padding: 24, - }, - contents: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - paddingTop: 50, - paddingBottom: 50, - }, - linkContainer: { - paddingLeft: 5, - }, -}); - -type Props = { - programId: string, - orgUnitId: string, - classes: Object, - children: React$Node, -}; - -const TrackerProgramHandler = ({ programId, orgUnitId, classes, children }: Props) => { - const { baseUrl } = useConfig(); - const getUrl = useCallback(() => { - const trackerBaseUrl = buildUrl(baseUrl, systemSettingsStore.get().trackerAppRelativePath, '/#/?'); - const params = `program=${programId}&ou=${orgUnitId}`; - return trackerBaseUrl + params; - }, [ - baseUrl, - programId, - orgUnitId, - ]); - - const program = getProgramFromProgramIdThrowIfNotFound(programId); - if (program instanceof TrackerProgram) { - return ( -
- -
- {i18n.t('To work with the selected program,')} - - - {i18n.t('open the Tracker Capture app')} - - -
-
-
- ); - } - - return children; -}; - -export const TrackerProgramHandlerComponent = withStyles(getStyles)(TrackerProgramHandler); diff --git a/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.container.js b/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.container.js deleted file mode 100644 index c95217bcba..0000000000 --- a/src/core_modules/capture-core/components/TrackerProgramHandler/TrackerProgramHandler.container.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import { connect } from 'react-redux'; -import { TrackerProgramHandlerComponent } from './TrackerProgramHandler.component'; - -const mapStateToProps = (state: ReduxState) => ({ - programId: state.currentSelections.programId, - orgUnitId: state.currentSelections.orgUnitId, -}); - -// $FlowSuppress -// $FlowFixMe[missing-annot] automated comment -export const TrackerProgramHandler = connect(mapStateToProps, () => ({}))(TrackerProgramHandlerComponent); diff --git a/src/core_modules/capture-core/components/TrackerProgramHandler/index.js b/src/core_modules/capture-core/components/TrackerProgramHandler/index.js deleted file mode 100644 index 3e231ffee6..0000000000 --- a/src/core_modules/capture-core/components/TrackerProgramHandler/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { TrackerProgramHandler } from './TrackerProgramHandler.container'; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js new file mode 100644 index 0000000000..e0b2ebb2dd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.js @@ -0,0 +1,84 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button, spacers, UserAvatar } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; +import type { Assignee } from './WidgetAssignee.types'; + +const styles = () => ({ + wrapper: { + display: 'flex', + alignItems: 'center', + fontSize: 14, + }, + editButton: { + marginLeft: spacers.dp12, + }, + assignButton: { + marginLeft: spacers.dp12, + }, + avatarWrapper: { + display: 'flex', + alignItems: 'center', + }, + avatar: { + margin: spacers.dp4, + }, +}); + +type Props = { + assignee: Assignee | null, + onEdit: () => {}, + writeAccess: boolean, + avatarId?: string, + ...CssClasses, +}; + +const DisplayModePlain = ({ assignee, onEdit, writeAccess, avatarId, classes }: Props) => ( + assignee ? ( +
+
+ {i18n.t('Assigned to')} + + {assignee.name} +
+ + + +
+ ) : ( +
+ {i18n.t('No one is assigned to this event')} + + + +
+ ) +); + +export const DisplayMode = withStyles(styles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js b/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js new file mode 100644 index 0000000000..dd7202f18c --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/EditMode.component.js @@ -0,0 +1,63 @@ +// @flow +import React, { useState } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { Button, ButtonStrip, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import type { Assignee } from './WidgetAssignee.types'; +import { UserField } from '../FormFields/UserField'; + +const styles = () => ({ + container: { + display: 'flex', + alignItems: 'center', + }, + searchContainer: { + flexGrow: 1, + flexShrink: 1, + paddingRight: 5, + }, + buttonContainer: { + marginTop: spacers.dp8, + }, +}); + +type Props = { + assignee: Assignee | null, + onCancel: () => {}, + onSet: (user: Assignee | null) => void, + ...CssClasses, +}; + +const EditModePlain = (props: Props) => { + const { onCancel, onSet, assignee, classes } = props; + const [tempUser, setTempUser] = useState(assignee); + + const onHandleSet = (user) => { + setTempUser(user); + }; + + return ( +
+
+ + + + + +
+
+ ); +}; + +export const EditMode = withStyles(styles)(EditModePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js new file mode 100644 index 0000000000..2b54a45fcd --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.js @@ -0,0 +1,62 @@ +// @flow +import React, { useState, useCallback } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { IconUser24, spacers } from '@dhis2/ui'; +import { withStyles } from '@material-ui/core/styles'; +import type { PlainProps } from './WidgetAssignee.types'; +import { Widget } from '../Widget'; +import { DisplayMode } from './DisplayMode.component'; +import { EditMode } from './EditMode.component'; + +const styles = () => ({ + header: { + display: 'flex', + alignItems: 'center', + }, + wrapper: { + padding: `0 ${spacers.dp16} ${spacers.dp16} ${spacers.dp16}`, + }, +}); + +const WidgetAssigneePlain = ({ assignee, writeAccess, onSet, avatarId, classes }: PlainProps) => { + const [open, setOpenStatus] = useState(true); + const [editMode, setEditMode] = useState(false); + + const handleSet = useCallback( + (user) => { + setEditMode(false); + onSet(user); + }, + [onSet], + ); + + return ( +
+ + {i18n.t('Assignee')} + + } + onOpen={useCallback(() => setOpenStatus(true), [setOpenStatus])} + onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} + open={open} + > +
+ {editMode ? ( + setEditMode(false)} onSet={handleSet} assignee={assignee} /> + ) : ( + setEditMode(true)} + writeAccess={writeAccess} + avatarId={avatarId} + /> + )} +
+
+
+ ); +}; + +export const WidgetAssigneeComponent = withStyles(styles)(WidgetAssigneePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js new file mode 100644 index 0000000000..53910eac42 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.js @@ -0,0 +1,50 @@ +// @flow +import React, { useCallback, useRef } from 'react'; +import { useDataMutation } from '@dhis2/app-runtime'; +import type { Props, Assignee } from './WidgetAssignee.types'; +import { WidgetAssigneeComponent } from './WidgetAssignee.component'; +import { convertClientToServer } from './converter'; +import { useUserAvatar } from './hooks'; + +const WidgetAssigneeWithHooks = (props: Props) => { + const { assignee, writeAccess, getSaveContext, onSave, onSaveError } = props; + const prevAssignee = useRef(assignee); + const { avatarId, isLoading } = useUserAvatar(assignee?.id); + + const [updateMutation] = useDataMutation( + { + resource: 'tracker?async=false&importStrategy=UPDATE', + type: 'create', + data: event => ({ events: [event] }), + }, + { + onError: () => { + onSaveError(prevAssignee.current); + }, + }, + ); + + const onSet = useCallback( + async (newAssignee: Assignee) => { + const { event } = getSaveContext(); + prevAssignee.current = assignee; + onSave(newAssignee); + await updateMutation({ ...event, assignedUser: convertClientToServer(newAssignee) }); + }, + [updateMutation, getSaveContext, onSave, assignee], + ); + + if (isLoading) { + return null; + } + + return ; +}; + +export const WidgetAssignee = (props: Props) => { + if (!props.enabled) { + return null; + } + + return ; +}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js new file mode 100644 index 0000000000..33eb25a570 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.js @@ -0,0 +1,26 @@ +// @flow + +export type Assignee = { + id: string, + username: string, + name: string, + firstName: string, + surname: string, +} + +export type Props = {| + assignee: Assignee | null, + enabled: boolean, + writeAccess: boolean, + getSaveContext: () => { event: ApiEnrollmentEvent }, + onSave: (newAssignee: Assignee) => void, + onSaveError: (prevAssignee: Assignee | null) => void, +|}; + +export type PlainProps = {| + assignee: Assignee | null, + writeAccess: boolean, + onSet: (user: Assignee | null) => void, + avatarId?: string, + ...CssClasses, +|}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/converter.js b/src/core_modules/capture-core/components/WidgetAssignee/converter.js new file mode 100644 index 0000000000..735b8ddfb2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/converter.js @@ -0,0 +1,14 @@ +// @flow +import type { Assignee } from './WidgetAssignee.types'; + +export const convertClientToServer = (assignee?: Assignee): ApiAssignedUser | null => ( + assignee + ? { + uid: assignee.id, + displayName: assignee.name, + username: assignee.username, + firstName: assignee.firstName, + surname: assignee.surname, + } + : null +); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js b/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js new file mode 100644 index 0000000000..d6927c9a30 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/hooks/index.js @@ -0,0 +1,2 @@ +// @flow +export { useUserAvatar } from './useUserAvatar'; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js b/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js new file mode 100644 index 0000000000..2e66e93561 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/hooks/useUserAvatar.js @@ -0,0 +1,20 @@ +// @flow +import { useApiMetadataQuery } from 'capture-core/utils/reactQueryHelpers'; + +export const useUserAvatar = (userId?: string) => { + const queryKey = ['users', ...(userId ? [userId] : [])]; + const queryFn = { + resource: 'users', + id: userId, + params: { + fields: 'avatar', + }, + }; + const queryOptions = { enabled: Boolean(userId) }; + const { data, isLoading } = useApiMetadataQuery(queryKey, queryFn, queryOptions); + + return { + avatarId: data?.avatar?.id, + isLoading, + }; +}; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/index.js b/src/core_modules/capture-core/components/WidgetAssignee/index.js new file mode 100644 index 0000000000..3118669d83 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetAssignee/index.js @@ -0,0 +1,3 @@ +// @flow +export { WidgetAssignee } from './WidgetAssignee.container'; + diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js index f1eeab8b32..db0b0e9af7 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/getConvertedAddEvent.js @@ -44,7 +44,7 @@ export const getAddEventEnrollmentServerData = ({ program: programId, programStage: formFoundation.id, orgUnit: orgUnitId, - trackedEntityInstance: teiId, + trackedEntity: teiId, enrollment: enrollmentId, scheduledAt: mainDataServerValues.occurredAt, orgUnitName, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js index 5542c3e8d4..3f39dd6b3f 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.js @@ -48,6 +48,7 @@ import { withDataEntryFields, } from '../../DataEntryDhis2Helpers/'; import { getProgramThrowIfNotFound, EventProgram } from '../../../metaData'; +import type { UserFormField } from '../../FormFields/UserField'; const tabMode = Object.freeze({ REPORT: 'REPORT', @@ -398,6 +399,7 @@ type Props = { eventStatus?: string, enrollmentId?: string, isCompleted?: boolean, + assignee?: UserFormField | null, }; @@ -458,6 +460,7 @@ class EditEventDataEntryPlain extends Component { classes, dataEntryId, onCancelEditEvent, + assignee, ...passOnProps } = this.props; @@ -485,6 +488,7 @@ class EditEventDataEntryPlain extends Component { orgUnitId={orgUnit.id} onSaveSuccessActionType={actionTypes.EVENT_SCHEDULE_SUCCESS} onSaveErrorActionType={actionTypes.EVENT_SCHEDULE_ERROR} + assignee={assignee} {...passOnProps} />} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.js b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.js index 999beb1aa7..7a518b9bfd 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.js @@ -4,7 +4,7 @@ import { map, filter, flatMap } from 'rxjs/operators'; import { batchActions } from 'redux-batched-actions'; import { dataEntryKeys, dataEntryIds } from 'capture-core/constants'; import moment from 'moment'; -import { EMPTY, of } from 'rxjs'; +import { EMPTY } from 'rxjs'; import { getFormattedStringFromMomentUsingEuropeanGlyphs } from 'capture-core-utils/date'; import { convertCategoryOptionsToServer, convertValue as convertToServerValue } from '../../../converters/clientToServer'; import { getProgramAndStageFromEvent, scopeTypes, getScopeInfo } from '../../../metaData'; @@ -38,7 +38,6 @@ import { buildUrlQueryString } from '../../../utils/routing/buildUrlQueryString' import { updateEventContainer, } from '../../Pages/ViewEvent/ViewEventComponent/viewEvent.actions'; -import { navigateToEnrollmentOverview } from '../../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { newEventWidgetActionTypes } from '../../WidgetEnrollmentEventNew/Validated/validated.actions'; const getDataEntryId = (event): string => ( @@ -220,6 +219,7 @@ export const startCreateNewAfterCompletingEpic = ( return EMPTY; } - return of(navigateToEnrollmentOverview(params)); + history.push(`/enrollment?${buildUrlQueryString(params)}`); + return EMPTY; })); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js index fb8bd883db..903bcf1333 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.js @@ -57,6 +57,7 @@ export const WidgetEventEditPlain = ({ orgUnitId, enrollmentId, teiId, + assignee, classes, }: PlainProps) => { const dispatch = useDispatch(); @@ -144,6 +145,7 @@ export const WidgetEventEditPlain = ({ allowGenerateNextVisit={programStage.allowGenerateNextVisit} availableProgramStages={availableProgramStages} hideDueDate={programStage.hideDueDate} + assignee={assignee} /> )} diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js index ebab5c3f30..df257a08a5 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js +++ b/src/core_modules/capture-core/components/WidgetEventEdit/widgetEventEdit.types.js @@ -1,6 +1,7 @@ // @flow import type { ProgramStage } from '../../metaData'; +import type { UserFormField } from '../FormFields/UserField'; export type Props = {| programStage: ProgramStage, @@ -13,6 +14,7 @@ export type Props = {| enrollmentId: string, teiId: string, initialScheduleDate?: string, + assignee?: UserFormField | null, |}; export type PlainProps = {| diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js index fdbbccd9f2..e9a1111920 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js @@ -4,4 +4,5 @@ import type { UserFormField } from '../../FormFields/UserField'; export type Props = { ...CssClasses, assignee?: UserFormField, + onSetAssignee: (user: UserFormField) => void, }; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js index 6dc9c0a5bc..cf7b02e10d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js @@ -35,7 +35,7 @@ export const requestScheduleEvent = ({ onSaveExternal: (eventServerValues: Object, uid: string) => void, onSaveSuccessActionType?: string, onSaveErrorActionType?: string, - assignedUser?: {uid: string}, + assignedUser?: ApiAssignedUser | null, }) => actionCreator(scheduleEventWidgetActionTypes.EVENT_SCHEDULE_REQUEST)({ scheduleDate, diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index 489ce42dbc..27cf5bc8b2 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import moment from 'moment'; -import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, getProgramEventAccess, dataElementTypes } from '../../metaData'; import { useOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import type { ContainerProps } from './widgetEventSchedule.types'; @@ -18,7 +18,7 @@ import { import { requestScheduleEvent } from './WidgetEventSchedule.actions'; import { NoAccess } from './AccessVerification'; import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; -import { convertAssigneeToServer } from '../../converters'; +import { convertClientToServer } from '../../converters'; export const WidgetEventSchedule = ({ enrollmentId, @@ -31,6 +31,8 @@ export const WidgetEventSchedule = ({ onSaveErrorActionType, onCancel, initialScheduleDate, + enableUserAssignment, + assignee: storedAssignee, ...passOnProps }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); @@ -44,7 +46,7 @@ export const WidgetEventSchedule = ({ const { currentUser, noteId } = useCommentDetails(); const [scheduleDate, setScheduleDate] = useState(''); const [comments, setComments] = useState([]); - const [assignee, setAssignee] = useState(); + const [assignee, setAssignee] = useState(storedAssignee); const { events } = useEventsInOrgUnit(orgUnitId, scheduleDate); const { eventId } = useLocationQuery(); const eventCountInOrgUnit = events @@ -57,6 +59,10 @@ export const WidgetEventSchedule = ({ if (!scheduleDate && suggestedScheduleDate) { setScheduleDate(suggestedScheduleDate); } }, [suggestedScheduleDate, scheduleDate]); + useEffect(() => { + setAssignee(storedAssignee); + }, [storedAssignee]); + const onHandleSchedule = useCallback(() => { if (programCategory?.categories && Object.keys(selectedCategories).length !== programCategory?.categories?.length) { @@ -82,7 +88,8 @@ export const WidgetEventSchedule = ({ onSaveExternal: onSave, onSaveSuccessActionType, onSaveErrorActionType, - ...(assignee && { assignedUser: convertAssigneeToServer(assignee) }), + // $FlowFixMe[incompatible-call] + ...(assignee && { assignedUser: convertClientToServer(assignee, dataElementTypes.ASSIGNEE) }), })); }, [ dispatch, @@ -170,7 +177,7 @@ export const WidgetEventSchedule = ({ programId={programId} programCategory={programCategory} programName={program.name} - enableUserAssignment={stage?.enableUserAssignment} + enableUserAssignment={enableUserAssignment && stage?.enableUserAssignment} scheduleDate={scheduleDate} displayDueDateLabel={programStageScheduleConfig.displayDueDateLabel} suggestedScheduleDate={suggestedScheduleDate} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js index f42a7e8f87..39b5789226 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js @@ -19,6 +19,8 @@ export type ContainerProps = {| onSaveSuccessActionType: string, onSaveErrorActionType: string, onCancel: () => void, + assignee?: UserFormField | null, + enableUserAssignment?: boolean, |}; export type Props = {| @@ -41,8 +43,8 @@ export type Props = {| categoryOptionsError?: ?{[categoryId: string]: { touched: boolean, valid: boolean} }, enableUserAssignment?: boolean, onSchedule: () => void, - onSetAssignee: (user: UserFormField) => void, - assignee?: UserFormField, + onSetAssignee: () => void, + assignee?: UserFormField | null, onCancel: () => void, setScheduleDate: (date: string) => void, onAddComment: (comment: string) => void, diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/isNotValidOptionSet.js b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/isNotValidOptionSet.js index ef970a412d..375e134dd6 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/isNotValidOptionSet.js +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/helpers/isNotValidOptionSet.js @@ -6,4 +6,4 @@ export const isNotValidOptionSet = ( optionSet?: ?{ +options: Array