From e33cd8738be9bbe1e3c34566fe07d7bf9fa9da6f Mon Sep 17 00:00:00 2001 From: Jennifer Q <66472237+latin-panda@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:59:13 +0700 Subject: [PATCH] feat(#9598): add training materials page (#9592) --- .../translations/messages-en.properties | 17 +- .../translations/messages-es.properties | 5 +- .../translations/messages-fr.properties | 5 +- .../translations/messages-hi.properties | 4 + .../translations/messages-id.properties | 4 + .../translations/messages-ne.properties | 5 +- .../translations/messages-sw.properties | 5 +- .../enketo/training-cards.wdio-spec.js | 5 +- .../service-worker.wdio-spec.js | 1 + tests/e2e/default/suites.js | 1 + .../forms/expired-training.xml | 95 ++++ .../forms/first-training.xml | 95 ++++ .../forms/second-training.xml | 95 ++++ .../training-materials.wdio-spec.js | 166 +++++++ .../default/common/common.wdio.page.js | 8 + .../enketo/training-cards.wdio.page.js | 49 +- webapp/src/css/content-list.less | 15 + webapp/src/css/inbox.less | 21 +- webapp/src/css/modal.less | 61 ++- webapp/src/css/old-nav.less | 16 + webapp/src/css/variables.less | 5 + webapp/src/img/icon-check.svg | 4 + webapp/src/ts/actions/global.ts | 15 + webapp/src/ts/app-routing.module.ts | 2 + webapp/src/ts/app.component.ts | 21 +- webapp/src/ts/components/components.module.ts | 3 + .../components/header/header.component.html | 6 + .../modal-layout/modal-layout.component.html | 2 +- .../sidebar-menu/sidebar-menu.component.ts | 6 + .../training-cards-form.component.html | 15 + .../training-cards-form.component.ts | 230 ++++++++++ webapp/src/ts/modals/modals.module.ts | 3 + .../training-cards-confirm.component.html | 12 + .../training-cards-confirm.component.ts | 33 ++ .../training-cards.component.html | 42 +- .../training-cards.component.ts | 222 ++------- webapp/src/ts/modules/modules.module.ts | 8 + .../src/ts/modules/tasks/tasks.component.html | 8 +- .../trainings-content.component.html | 21 + .../trainings/trainings-content.component.ts | 112 +++++ .../trainings-route.guard.provider.ts | 18 + .../trainings/trainings.component.html | 42 ++ .../modules/trainings/trainings.component.ts | 129 ++++++ .../ts/modules/trainings/trainings.routes.ts | 28 ++ webapp/src/ts/reducers/global.ts | 5 + webapp/src/ts/selectors/index.ts | 1 + .../src/ts/services/training-cards.service.ts | 132 ++++-- webapp/src/ts/training-card.guard.provider.ts | 2 +- .../sidebar-menu.component.spec.ts | 6 + .../training-cards-form.component.spec.ts | 351 +++++++++++++++ .../training-cards.component.spec.ts | 394 ++-------------- .../trainings-content.component.spec.ts | 173 +++++++ .../trainings/trainings.component.spec.ts | 200 ++++++++ webapp/tests/karma/ts/selectors/index.spec.ts | 1 + .../services/training-cards.service.spec.ts | 426 +++++++++++------- 55 files changed, 2514 insertions(+), 837 deletions(-) create mode 100644 tests/e2e/default/training-materials/forms/expired-training.xml create mode 100644 tests/e2e/default/training-materials/forms/first-training.xml create mode 100644 tests/e2e/default/training-materials/forms/second-training.xml create mode 100644 tests/e2e/default/training-materials/training-materials.wdio-spec.js create mode 100644 webapp/src/img/icon-check.svg create mode 100644 webapp/src/ts/components/training-cards-form/training-cards-form.component.html create mode 100644 webapp/src/ts/components/training-cards-form/training-cards-form.component.ts create mode 100644 webapp/src/ts/modals/training-cards-confirm/training-cards-confirm.component.html create mode 100644 webapp/src/ts/modals/training-cards-confirm/training-cards-confirm.component.ts create mode 100644 webapp/src/ts/modules/trainings/trainings-content.component.html create mode 100644 webapp/src/ts/modules/trainings/trainings-content.component.ts create mode 100644 webapp/src/ts/modules/trainings/trainings-route.guard.provider.ts create mode 100644 webapp/src/ts/modules/trainings/trainings.component.html create mode 100644 webapp/src/ts/modules/trainings/trainings.component.ts create mode 100644 webapp/src/ts/modules/trainings/trainings.routes.ts create mode 100644 webapp/tests/karma/ts/components/training-cards-form/training-cards-form.component.spec.ts create mode 100644 webapp/tests/karma/ts/modules/trainings/trainings-content.component.spec.ts create mode 100644 webapp/tests/karma/ts/modules/trainings/trainings.component.spec.ts diff --git a/api/resources/translations/messages-en.properties b/api/resources/translations/messages-en.properties index cb04a0a6e72..3bc93546cdc 100644 --- a/api/resources/translations/messages-en.properties +++ b/api/resources/translations/messages-en.properties @@ -437,7 +437,7 @@ configuration.sms = SMS configuration.sms.forms = SMS forms configuration.sms.forms.title = You need to choose both an XML file and a Meta file before clicking the upload button. You may only upload one app form file at a time and any existing forms will be overwritten. configuration.sms.settings = Basic settings -configuration.sms.test.description = Use this page to send a test message to the production application without going through the SMS Gateway. Be sure to use a phone number registered to a CHW’s profile to mimic a report coming from him or her about a particular patient. +configuration.sms.test.description = Use this page to send a test message to the production application without going through the SMS Gateway. Be sure to use a phone number registered to a CHW?s profile to mimic a report coming from him or her about a particular patient. configuration.sms.test.from.number = From phone number configuration.sms.test.message.description = Limit of 144 characters configuration.sms.test.number.validation.description = Please enter a valid phone number without dashes or punctuation. @@ -454,7 +454,7 @@ confirm.destructive.navigation.submit = Exit confirm.destructive.navigation.title = Exit form? confirm.logout = You will need an internet connection to log back in. password.updated = Your password has been successfully updated. -confirm.verification = This report will be verified as “correct”. This cannot be changed later. +confirm.verification = This report will be verified as ?correct?. This cannot be changed later. confirm.verification.submit = Verify as correct confirm.verification.title = Verify report contact.age = Age @@ -654,8 +654,8 @@ enketo.geopicker.altitude = altitude (m) enketo.geopicker.closepolygon = close polygon enketo.geopicker.kmlcoords = KML coordinates enketo.geopicker.kmlpaste = paste KML coordinates here -enketo.geopicker.latitude = latitude (x.y °) -enketo.geopicker.longitude = longitude (x.y °) +enketo.geopicker.latitude = latitude (x.y ) +enketo.geopicker.longitude = longitude (x.y ) enketo.geopicker.points = points enketo.geopicker.searchPlaceholder = search for place or address enketo.geopicker.removePoint = This will completely remove the current geopoint from the list of geopoints and cannot be undone. Are you sure you want to do this? @@ -686,7 +686,7 @@ export.dhis.place.all = All Places export.dhis.place.description = Filter exported data to include data associated with contacts under this place in the hierarchy. export.dhis.place.label = Filter by place export.dhis.unconfigured = DHIS2 integration is not configured. -export.feedback.description = Download a log of detected errors and user feedback submitted via the “Report bug” feature in CSV format. The table below shows the most recently submitted reports. +export.feedback.description = Download a log of detected errors and user feedback submitted via the ?Report bug? feature in CSV format. The table below shows the most recently submitted reports. export.messages.description = Download all messages that have ever been sent or received in CSV format. export.people.description = Download all contacts registered in the system in JSON format. export.reports.description = Download a summary of all the reports that have ever been submitted in CSV format. @@ -1215,7 +1215,7 @@ sync.last_success = Last sync sync.now = Sync now sync.retry = Retry sync.feedback.failure.unknown = Sync failed. Unable to connect. -sync.status.in_progress = Currently syncing… +sync.status.in_progress = Currently syncing? sync.status.not_required = All reports synced sync.status.required = Reports to sync sync.status.unknown = Unable to connect @@ -1264,7 +1264,10 @@ training_cards.confirm.title = Leave training? training_cards.error.loading = Error loading training. Please contact your supervisor. training_cards.error.save = Error saving training. training_cards.form.saved = Training completed. -training_cards.modal.title = Important changes +training_materials.page.no_more_trainings = No more trainings +training_materials.page.no_selected = No training material selected +training_materials.page.no_trainings = No trainings found +training_materials.page.title = Training materials translation.add = Add new translation key translation.key = Translation key unique.id = Unique ID diff --git a/api/resources/translations/messages-es.properties b/api/resources/translations/messages-es.properties index 2bb18b9bcf7..4dcc0aa8506 100644 --- a/api/resources/translations/messages-es.properties +++ b/api/resources/translations/messages-es.properties @@ -1264,7 +1264,10 @@ training_cards.confirm.title = \¿Salir del entrenamiento\? training_cards.error.loading = Hubo un error al cargar el entrenamiento. Por favor contacte a su supervisor. training_cards.error.save = Hubo un error al guardar el entrenamiento. training_cards.form.saved = Entrenamiento completado. -training_cards.modal.title = Cambios importantes +training_materials.page.no_more_trainings = No hay más entrenamientos +training_materials.page.no_selected = Ningún material de entrenamiento seleccionado +training_materials.page.no_trainings = No se encontraron entrenamientos +training_materials.page.title = Materiales de entrenamiento translation.add = Agregar Traducción translation.key = Clave de traducción unique.id = Identificación única diff --git a/api/resources/translations/messages-fr.properties b/api/resources/translations/messages-fr.properties index a82b00af6de..87b2ddd1e14 100644 --- a/api/resources/translations/messages-fr.properties +++ b/api/resources/translations/messages-fr.properties @@ -1264,7 +1264,10 @@ training_cards.confirm.title = Quitter l'entraînement? training_cards.error.loading = Erreur lors du chargement de la formation. Veuillez contacter votre superviseur. training_cards.error.save = Erreur lors de l'enregistrement de la formation. training_cards.form.saved = Formation terminée. -training_cards.modal.title = Changements importants +training_materials.page.no_more_trainings = Aucune formation restante +training_materials.page.no_selected = Aucun matériel de formation sélectionné +training_materials.page.no_trainings = Aucune formation trouvée +training_materials.page.title = Matériel de formation translation.add = Ajouter une traduction translation.key = Clé de traduction unique.id = ID unique diff --git a/api/resources/translations/messages-hi.properties b/api/resources/translations/messages-hi.properties index 9b012194aff..e39d2bf0510 100644 --- a/api/resources/translations/messages-hi.properties +++ b/api/resources/translations/messages-hi.properties @@ -1168,6 +1168,10 @@ training_cards.confirm.exit = यह प्रशिक्षण समाप् training_cards.confirm.button.no = रद्द करें training_cards.confirm.button.yes = बाहर निकलें training_cards.confirm.title = प्रशिक्षण छोड़ें? +training_materials.page.no_more_trainings = अब और कोई प्रशिक्षण नहीं है +training_materials.page.no_selected = कोई प्रशिक्षण दस्तावेज़ नहीं चुना गया +training_materials.page.no_trainings = कोई प्रशिक्षण नहीं मिला +training_materials.page.title = प्रशिक्षण दस्तावेज़ translation.add = अनुवाद दर्ज करें translation.key = अनुवाद का गाइड unique.id = diff --git a/api/resources/translations/messages-id.properties b/api/resources/translations/messages-id.properties index 53a0b392f54..28e9a589877 100644 --- a/api/resources/translations/messages-id.properties +++ b/api/resources/translations/messages-id.properties @@ -1175,6 +1175,10 @@ training_cards.confirm.exit = Pelatihan ini belum selesai. Jika Anda keluar seka training_cards.confirm.button.no = Batalkan training_cards.confirm.button.yes = Keluar training_cards.confirm.title = Keluar dari pelatihan? +training_materials.page.no_more_trainings = Tidak ada lagi pelatihan +training_materials.page.no_selected = Tidak ada materi pelatihan yang dipilih +training_materials.page.no_trainings = Tidak ditemukan pelatihan +training_materials.page.title = Materi pelatihan translation.add = Tambah terjemahan translation.key = Kunci terjemahan unique.id = diff --git a/api/resources/translations/messages-ne.properties b/api/resources/translations/messages-ne.properties index ab858cbc35b..78fa02deecf 100644 --- a/api/resources/translations/messages-ne.properties +++ b/api/resources/translations/messages-ne.properties @@ -1264,7 +1264,10 @@ training_cards.confirm.title = तालिम छोड्ने हो? training_cards.error.loading = तालिम लोड गर्दा त्रुटि भयो। तपाइँको सुपरभाइजरलाई सम्पर्क गर्नुहोस्। training_cards.error.save = तालिम सेभ गर्न त्रुटि। training_cards.form.saved = तालिम सम्पन्न भयो। -training_cards.modal.title = महत्वपुर्ण परिवर्तनहरु +training_materials.page.no_more_trainings = थप तालिम छैन +training_materials.page.no_selected = कुनै तालिम सामग्री छनोट गरिएको छैन। +training_materials.page.no_trainings = तालिम फेला परेन +training_materials.page.title = तालिम शीर्षक translation.add = नयाँ अनुवाद कुञ्जी थप्नुहोस् translation.key = अनुवाद कुञ्जी unique.id = आईडी diff --git a/api/resources/translations/messages-sw.properties b/api/resources/translations/messages-sw.properties index b7e0f9dae7e..13c4eadb09e 100644 --- a/api/resources/translations/messages-sw.properties +++ b/api/resources/translations/messages-sw.properties @@ -1264,7 +1264,10 @@ training_cards.confirm.title = Ungependa kuondoka kwenye mafunzo? training_cards.error.loading = Hitilafu katika kupakia mafunzo. Tafadhali wasiliana na msimamizi wako. training_cards.error.save = Hitilafu katika kuhifadhi mafunzo. training_cards.form.saved = Mafunzo yamekamilika. -training_cards.modal.title = Mabadiliko muhimu +training_materials.page.no_more_trainings = Hakuna mafunzo mengine +training_materials.page.no_selected = Hakuna nyenzo za mafunzo zilizochaguliwa +training_materials.page.no_trainings = Hakuna mafunzo yaliyopatikana +training_materials.page.title = Vifaa vya mafunzo translation.add = Ongeza tafsiri translation.key = Ufunguo wa tafsiri unique.id = Kitambulisho cha kipekee diff --git a/tests/e2e/default/enketo/training-cards.wdio-spec.js b/tests/e2e/default/enketo/training-cards.wdio-spec.js index b9bd689ba2f..1a5ff4d781c 100644 --- a/tests/e2e/default/enketo/training-cards.wdio-spec.js +++ b/tests/e2e/default/enketo/training-cards.wdio-spec.js @@ -4,7 +4,6 @@ const commonPage = require('@page-objects/default/common/common.wdio.page'); const trainingCardsPage = require('@page-objects/default/enketo/training-cards.wdio.page'); const placeFactory = require('@factories/cht/contacts/place'); const userFactory = require('@factories/cht/users/users'); -const personFactory = require('@factories/cht/contacts/person'); const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); const privacyPolicyFactory = require('@factories/cht/settings/privacy-policy'); const privacyPage = require('@page-objects/default/privacy-policy/privacy-policy.wdio.page'); @@ -26,7 +25,6 @@ describe('Training Cards', () => { before(async () => { const parent = placeFactory.place().build({ _id: 'dist1', type: 'district_hospital' }); const user = userFactory.build({ roles: [ 'nurse', 'chw' ] }); - const patient = personFactory.build({ parent: { _id: user.place._id, parent: { _id: parent._id } } }); const formDoc = commonPage.createFormDoc(`${__dirname}/forms/training-cards-text-only`); formDoc._id = `form:${formDocId}`; formDoc.internalId = formDocId; @@ -36,8 +34,7 @@ describe('Training Cards', () => { duration: 5, }; - await utils.saveDocs([ parent, patient ]); - await utils.saveDoc(formDoc); + await utils.saveDocs([ parent, formDoc ]); await utils.createUsers([ user ]); await loginPage.login(user); await commonPage.waitForPageLoaded(); diff --git a/tests/e2e/default/service-worker/service-worker.wdio-spec.js b/tests/e2e/default/service-worker/service-worker.wdio-spec.js index 3fe432351a0..4c1cf4c3302 100644 --- a/tests/e2e/default/service-worker/service-worker.wdio-spec.js +++ b/tests/e2e/default/service-worker/service-worker.wdio-spec.js @@ -120,6 +120,7 @@ describe('Service worker cache', () => { '/img/icon-pregnant-selected.svg', '/img/icon-pregnant.svg', '/img/icon-filter.svg', + '/img/icon-check.svg', '/img/icon.png', '/img/icon-back.svg', '/img/layers.png', diff --git a/tests/e2e/default/suites.js b/tests/e2e/default/suites.js index 7514c73f9b1..d0254acd85b 100644 --- a/tests/e2e/default/suites.js +++ b/tests/e2e/default/suites.js @@ -6,6 +6,7 @@ const SUITES = { './more-options-menu/**/*.wdio-spec.js', './users/**/*.wdio-spec.js', './about/**/*.wdio-spec.js', + './training-materials/**/*.wdio-spec.js', './navigation/**/*.wdio-spec.js', './old-navigation/**/*.wdio-spec.js', './privacy-policy/**/*.wdio-spec.js', diff --git a/tests/e2e/default/training-materials/forms/expired-training.xml b/tests/e2e/default/training-materials/forms/expired-training.xml new file mode 100644 index 00000000000..9461ad67964 --- /dev/null +++ b/tests/e2e/default/training-materials/forms/expired-training.xml @@ -0,0 +1,95 @@ + + + + First Training + + + + + **Old feature** + + + **New feature** + + + The "New Action" icon at the bottom of your app has also changed. + + + If you do not understand these changes, please contact your supervisor. + + + When you're ready, go ahead and start using your app. + + + There have been some changes to icons in your app. The next few screens will show you the difference. + + + Read each screen carefully and tap "Next" if you understand. If you need extra support, please contact your supervisor. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/e2e/default/training-materials/forms/first-training.xml b/tests/e2e/default/training-materials/forms/first-training.xml new file mode 100644 index 00000000000..3f50b1d7c07 --- /dev/null +++ b/tests/e2e/default/training-materials/forms/first-training.xml @@ -0,0 +1,95 @@ + + + + First Training + + + + + **Old feature** + + + **New feature** + + + The "New Action" icon at the bottom of your app has also changed. + + + If you do not understand these changes, please contact your supervisor. + + + When you're ready, go ahead and start using your app. + + + There have been some changes to icons in your app. The next few screens will show you the difference. + + + Read each screen carefully and tap "Next" if you understand. If you need extra support, please contact your supervisor. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/e2e/default/training-materials/forms/second-training.xml b/tests/e2e/default/training-materials/forms/second-training.xml new file mode 100644 index 00000000000..9bebba6a873 --- /dev/null +++ b/tests/e2e/default/training-materials/forms/second-training.xml @@ -0,0 +1,95 @@ + + + + Second Training + + + + + **Old icon** + + + **New icon** + + + The "New Action" icon at the bottom of your app has also changed. + + + If you do not understand these changes, please contact your supervisor. + + + When you're ready, go ahead and start using your app. + + + The next few screens will show you the difference. + + + Read each screen carefully and tap "Next" if you understand. If you need extra support, please contact your supervisor. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/e2e/default/training-materials/training-materials.wdio-spec.js b/tests/e2e/default/training-materials/training-materials.wdio-spec.js new file mode 100644 index 00000000000..26490843d3e --- /dev/null +++ b/tests/e2e/default/training-materials/training-materials.wdio-spec.js @@ -0,0 +1,166 @@ +const fs = require('fs'); +const utils = require('@utils'); +const loginPage = require('@page-objects/default/login/login.wdio.page'); +const commonPage = require('@page-objects/default/common/common.wdio.page'); +const trainingCardsPage = require('@page-objects/default/enketo/training-cards.wdio.page'); +const placeFactory = require('@factories/cht/contacts/place'); +const userFactory = require('@factories/cht/users/users'); +const commonElements = require('@page-objects/default/common/common.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); + +describe('Training Materials Page', () => { + const CONFIRM_TITLE = 'Leave training?'; + const CONFIRM_CONTENT = 'This training is not finished. ' + + 'If you leave now, you will lose your progress and be prompted again later to complete it.'; + const FORMS_FOLDER = `${__dirname}/../../../e2e/default/training-materials/forms`; + const FIRST_TRAINING_NAME = 'first_training'; + const FIRST_TRAINING_ID = `training:${FIRST_TRAINING_NAME}`; + const SECOND_TRAINING_NAME = 'second_training'; + const SECOND_TRAINING_ID = `training:${SECOND_TRAINING_NAME}`; + + before(async () => { + const facility = placeFactory.place().build({ _id: 'dist1', type: 'district_hospital' }); + const user = userFactory.build({ roles: [ 'pharmacist', 'chw' ] }); + + const firstXML = fs.readFileSync(`${FORMS_FOLDER}/first-training.xml`, 'utf8'); + const firstTraining = { + _id: `form:${FIRST_TRAINING_ID}`, + internalId: FIRST_TRAINING_ID, + title: FIRST_TRAINING_NAME, + type: 'form', + context: { start_date: new Date().getTime(), user_roles: [ 'pharmacist' ], duration: 5 }, + _attachments: { + xml: { content_type: 'application/octet-stream', data: Buffer.from(firstXML).toString('base64') }, + }, + }; + + const secondXML = fs.readFileSync(`${FORMS_FOLDER}/second-training.xml`, 'utf8'); + const secondTraining = { + _id: `form:${SECOND_TRAINING_ID}`, + internalId: SECOND_TRAINING_ID, + title: SECOND_TRAINING_NAME, + type: 'form', + context: { start_date: new Date().getTime(), user_roles: [ 'pharmacist' ], duration: 5 }, + _attachments: { + xml: { content_type: 'application/octet-stream', data: Buffer.from(secondXML).toString('base64') }, + }, + }; + + const expiredTrainingXML = fs.readFileSync(`${FORMS_FOLDER}/expired-training.xml`, 'utf8'); + const expiredTraining = { + _id: 'form:training:expired_training', + internalId: 'training:expired_training', + title: 'expired_training', + type: 'form', + context: { start_date: '2023-12-8', user_roles: [ 'pharmacist' ], duration: 50 }, + _attachments: { + xml: { content_type: 'application/octet-stream', data: Buffer.from(expiredTrainingXML).toString('base64') }, + }, + }; + + await utils.saveDocs([ facility, firstTraining, expiredTraining, secondTraining ]); + await utils.createUsers([ user ]); + await loginPage.login(user); + await commonElements.waitForPageLoaded(); + }); + + it('should quit training in modal, and be able to complete it later in the Training Material page,' + + ' verify completed trainings display in the list', async () => { + await trainingCardsPage.waitForTrainingCards(); + const trainingModalTitle = await trainingCardsPage.getTrainingTitle(); + expect(trainingModalTitle).to.equal(FIRST_TRAINING_NAME); + + const confirmMessage = await trainingCardsPage.quitTraining(); + expect(confirmMessage.header).to.equal(CONFIRM_TITLE); + expect(confirmMessage.body).to.contain(CONFIRM_CONTENT); + await trainingCardsPage.confirmQuitTraining(); + await trainingCardsPage.checkTrainingCardIsNotDisplayed(); + + await commonPage.openHamburgerMenu(); + await commonPage.openTrainingMaterials(); + + const trainings = await trainingCardsPage.getAllTrainingsText(); + expect(trainings.length).to.equal(2); + expect(trainings).to.have.members([ FIRST_TRAINING_NAME, SECOND_TRAINING_NAME ]); + expect(await trainingCardsPage.isTrainingComplete(FIRST_TRAINING_ID)).to.be.false; + expect(await trainingCardsPage.isTrainingComplete(SECOND_TRAINING_ID)).to.be.false; + + await trainingCardsPage.openTrainingMaterial(FIRST_TRAINING_ID); + const trainingMaterialTitle = await trainingCardsPage.getTrainingTitle(); + expect(trainingMaterialTitle).to.equal(FIRST_TRAINING_NAME); + + const introCard = await trainingCardsPage.getCardContent(FIRST_TRAINING_NAME, 'intro/intro_note_1:label"]'); + expect(introCard).to.equal( + 'There have been some changes to icons in your app. The next few screens will show you the difference.' + ); + const nextCard = await trainingCardsPage.getNextCardContent( + FIRST_TRAINING_NAME, + 'action_icons/action_icons_note_1:label"]', + ); + expect(nextCard).to.equal('The "New Action" icon at the bottom of your app has also changed.'); + const lastCard = await trainingCardsPage.getNextCardContent(FIRST_TRAINING_NAME, 'ending/ending_note_1:label"]'); + expect(lastCard).to.equal('If you do not understand these changes, please contact your supervisor.'); + await trainingCardsPage.submitTraining(false); + + const allTrainings = await trainingCardsPage.getAllTrainingsText(); + expect(allTrainings.length).to.equal(2); + expect(allTrainings).to.have.members([ FIRST_TRAINING_NAME, SECOND_TRAINING_NAME ]); + expect(await trainingCardsPage.isTrainingComplete(FIRST_TRAINING_ID)).to.be.true; + expect(await trainingCardsPage.isTrainingComplete(SECOND_TRAINING_ID)).to.be.false; + + await commonPage.goToReports(); + const firstReport = await reportsPage.getListReportInfo(await reportsPage.leftPanelSelectors.firstReport()); + expect(firstReport.form).to.equal(FIRST_TRAINING_ID); + }); + + it('should revisit completed trainings and load uncompleted trainings', async () => { + await commonPage.openHamburgerMenu(); + await commonPage.openTrainingMaterials(); + + const trainings = await trainingCardsPage.getAllTrainingsText(); + expect(trainings.length).to.equal(2); + expect(trainings).to.have.members([ FIRST_TRAINING_NAME, SECOND_TRAINING_NAME ]); + expect(await trainingCardsPage.isTrainingComplete(FIRST_TRAINING_ID)).to.be.true; + expect(await trainingCardsPage.isTrainingComplete(SECOND_TRAINING_ID)).to.be.false; + + await trainingCardsPage.openTrainingMaterial(FIRST_TRAINING_ID); + const trainingMaterialTitle = await trainingCardsPage.getTrainingTitle(); + expect(trainingMaterialTitle).to.equal(FIRST_TRAINING_NAME); + + const introCard = await trainingCardsPage.getCardContent(FIRST_TRAINING_NAME, 'intro/intro_note_1:label"]'); + expect(introCard).to.equal( + 'There have been some changes to icons in your app. The next few screens will show you the difference.' + ); + const nextCard = await trainingCardsPage.getNextCardContent( + FIRST_TRAINING_NAME, + 'action_icons/action_icons_note_1:label"]', + ); + expect(nextCard).to.equal('The "New Action" icon at the bottom of your app has also changed.'); + + const confirmMessage = await trainingCardsPage.quitTraining(); + expect(confirmMessage.header).to.equal(CONFIRM_TITLE); + expect(confirmMessage.body).to.contain(CONFIRM_CONTENT); + await trainingCardsPage.confirmQuitTraining(); + await trainingCardsPage.checkTrainingCardIsNotDisplayed(); + + await trainingCardsPage.openTrainingMaterial(SECOND_TRAINING_ID); + const secondTrainingTitle = await trainingCardsPage.getTrainingTitle(); + expect(secondTrainingTitle).to.equal(SECOND_TRAINING_NAME); + const secondIntroCard = await trainingCardsPage.getCardContent(SECOND_TRAINING_NAME, 'intro/intro_note_1:label"]'); + expect(secondIntroCard).to.equal( + 'The next few screens will show you the difference.' + ); + + const secondConfirmMessage = await trainingCardsPage.quitTraining(); + expect(secondConfirmMessage.header).to.equal(CONFIRM_TITLE); + expect(secondConfirmMessage.body).to.contain(CONFIRM_CONTENT); + await trainingCardsPage.confirmQuitTraining(); + await trainingCardsPage.checkTrainingCardIsNotDisplayed(); + + const allTrainings = await trainingCardsPage.getAllTrainingsText(); + expect(allTrainings.length).to.equal(2); + expect(allTrainings).to.have.members([ FIRST_TRAINING_NAME, SECOND_TRAINING_NAME ]); + expect(await trainingCardsPage.isTrainingComplete(FIRST_TRAINING_ID)).to.be.true; + expect(await trainingCardsPage.isTrainingComplete(SECOND_TRAINING_ID)).to.be.false; + }); +}); diff --git a/tests/page-objects/default/common/common.wdio.page.js b/tests/page-objects/default/common/common.wdio.page.js index 4c196d6f2f1..14579e7724f 100644 --- a/tests/page-objects/default/common/common.wdio.page.js +++ b/tests/page-objects/default/common/common.wdio.page.js @@ -20,6 +20,7 @@ const hamburgerMenuSelectors = { syncSuccess: () => $('aria/All reports synced'), syncInProgress: () => $('mat-sidenav-content').$('*="Currently syncing"'), aboutButton: () => $('aria/About'), + trainingMaterialsButton: () => $('aria/Training materials'), userSettingsButton: () => $('aria/User settings'), feedbackMenuOption: () => $('aria/Report bug'), logoutButton: () => $('aria/Log out'), @@ -436,6 +437,12 @@ const openUserSettingsAndFetchProperties = async () => { await (await userSettingsSelectors.editProfileButton()).waitForDisplayed(); }; +const openTrainingMaterials = async () => { + await (await hamburgerMenuSelectors.trainingMaterialsButton()).waitForClickable(); + await (await hamburgerMenuSelectors.trainingMaterialsButton()).click(); + await waitForPageLoaded(); +}; + const openEditProfile = async () => { await (await userSettingsSelectors.editProfileButton()).waitForClickable(); await (await userSettingsSelectors.editProfileButton()).click(); @@ -555,6 +562,7 @@ module.exports = { closeReportBug, openAboutMenu, openUserSettings, + openTrainingMaterials, openUserSettingsAndFetchProperties, openEditProfile, openAppManagement, diff --git a/tests/page-objects/default/enketo/training-cards.wdio.page.js b/tests/page-objects/default/enketo/training-cards.wdio.page.js index a88d0579cdb..8726e8e91f9 100644 --- a/tests/page-objects/default/enketo/training-cards.wdio.page.js +++ b/tests/page-objects/default/enketo/training-cards.wdio.page.js @@ -1,11 +1,18 @@ const genericFormPage = require('./generic-form.wdio.page'); const modalPage = require('@page-objects/default/common/modal.wdio.page'); +const commonElements = require('@page-objects/default/common/common.wdio.page'); -const ENKETO_MODAL = '.enketo-modal'; - -const trainingCardsForm = () => $(ENKETO_MODAL); +const trainingCardsForm = () => $('#training-cards-form'); const cardText = (context, field) => $(`.question-label[lang="en"][data-itext-id="/${context}/${field}`); -const quitTrainingBtn = () => $(`${ENKETO_MODAL} .item-content button[test-id="quit-training"]`); +const quitTrainingBtn = () => $('.item-content button[test-id="quit-training"]'); + +const TRAINING_LIST_ID = '#trainings-list'; +const ALL_TRAININGS = `${TRAINING_LIST_ID} li.content-row`; +const leftPanelSelectors = { + allTrainings: () => $$(ALL_TRAININGS), + trainingRowsText: () => $$(`${ALL_TRAININGS} .heading h4 span`), + trainingByUUID: (uuid) => $(`${TRAINING_LIST_ID} li.content-row[data-record-id="${uuid}"]`), +}; const waitForTrainingCards = async () => { await (await trainingCardsForm()).waitForDisplayed(); @@ -15,6 +22,12 @@ const checkTrainingCardIsNotDisplayed = async () => { await (await trainingCardsForm()).waitForDisplayed({ reverse: true }); }; +const getTrainingTitle = async () => { + const title = await $('#form-title'); + await title.waitForDisplayed(); + return title.getText(); +}; + const getCardContent = async (context, field) => { return await (await cardText(context, field)).getText(); }; @@ -35,17 +48,37 @@ const confirmQuitTraining = async () => { await modalPage.checkModalHasClosed(); }; -const submitTraining = async () => { +const submitTraining = async (checkModal = true) => { await genericFormPage.submitForm(); - await modalPage.checkModalHasClosed(); + if (checkModal) { + await modalPage.checkModalHasClosed(); + } +}; + +const openTrainingMaterial = async (formID) => { + await (await leftPanelSelectors.trainingByUUID(formID)).waitForClickable(); + await (await leftPanelSelectors.trainingByUUID(formID)).click(); +}; + +const getAllTrainingsText = async () => { + await (await leftPanelSelectors.allTrainings()[0]).waitForDisplayed(); + return commonElements.getTextForElements(leftPanelSelectors.trainingRowsText); +}; + +const isTrainingComplete = async (formId) => { + return (await $(`[data-record-id="${formId}"] .mat-icon-check`)).isExisting(); }; module.exports = { checkTrainingCardIsNotDisplayed, - waitForTrainingCards, + confirmQuitTraining, + getAllTrainingsText, getCardContent, getNextCardContent, + getTrainingTitle, + isTrainingComplete, + openTrainingMaterial, quitTraining, - confirmQuitTraining, submitTraining, + waitForTrainingCards, }; diff --git a/webapp/src/css/content-list.less b/webapp/src/css/content-list.less index 9df79d09088..9401940fdcf 100644 --- a/webapp/src/css/content-list.less +++ b/webapp/src/css/content-list.less @@ -217,6 +217,21 @@ } } +.trainings .content-row { + &.selected, &.selected-to-view { + background-color: @training-highlight; + } + + &:hover { + border-left-color: @training-color; + } + + &.completed .mat-icon-check { + padding-top: 6px; + color: @completed-state-color; + } +} + .contacts .content-row { &.selected { background-color: @contacts-highlight; diff --git a/webapp/src/css/inbox.less b/webapp/src/css/inbox.less index 71a71b4af99..8e324ff468a 100644 --- a/webapp/src/css/inbox.less +++ b/webapp/src/css/inbox.less @@ -53,18 +53,20 @@ body { } &.about, + &.trainings, &.testing, &.user, &.privacy-policy { .tool-bar { background-color: @top-header-color; - .ellipsis-title { + .ellipsis-title, + .app-menu-button .mat-icon[fonticon="fa-bars"]:before { color: @text-inverse-color; } - .app-menu-button .mat-icon[fonticon="fa-bars"]:before { - color: @text-inverse-color; + .navigation .mat-icon-back path { + fill: @text-inverse-color; } } @@ -479,7 +481,10 @@ mm-analytics-filters { background-color: @background-color; } -.reports .inbox-items, .contacts .inbox-items, .messages .inbox-items { +.reports .inbox-items, +.contacts .inbox-items, +.messages .inbox-items, +.trainings .inbox-items { overflow-y: hidden; overflow-x: hidden; height: 100%; @@ -846,6 +851,7 @@ a.fa:hover { .reports, .messages, +.trainings, .tasks { .item-content { .body { @@ -1376,6 +1382,11 @@ mm-sidebar-menu .mat-sidenav-container { margin-left: 10px; flex-shrink: 0; } + + mat-icon.fa-graduation-cap:before { + font-size: @font-medium; + vertical-align: middle; + } } .nav-item:not(:not(.hidden) ~ .nav-item) { // The first element without .hidden class, compatible with Chrome +90 @@ -1432,6 +1443,7 @@ mm-sidebar-menu .mat-sidenav-container { &.messages, &.tasks, &.reports, + &.trainings, &.contacts { .loading-status { margin: 10px 10px calc(@tab-navbar-size + 10px); @@ -1667,6 +1679,7 @@ mm-sidebar-menu .mat-sidenav-container { } } .reports, + .trainings, .tasks { .content .item-content .body > div > ul > li { padding: 10px; diff --git a/webapp/src/css/modal.less b/webapp/src/css/modal.less index 5e2dc806890..80e70b07d6f 100644 --- a/webapp/src/css/modal.less +++ b/webapp/src/css/modal.less @@ -36,6 +36,7 @@ mm-modal-layout { .modal-footer { padding: 24px 0; + margin-bottom: 0; } } @@ -55,7 +56,8 @@ mm-modal-layout { text-align: left; } - .enketo-modal { + .enketo-modal, + .modal-body training-cards-confirm { &.content-pane { position: relative; } @@ -63,15 +65,13 @@ mm-modal-layout { .item-content { padding: 0; background: none; + & > .body { + margin: 0; + } } .modal-body { overflow: hidden; - margin-bottom: 11px; - } - - .modal-footer { - padding: 0; } .enketo { @@ -100,18 +100,31 @@ mm-modal-layout { } } - form { - max-height: 65vh; + form.pages [role=page].current { + margin: 0; + display: block; + max-height: @training-cards-page-height; overflow-y: auto; + padding-bottom: @training-cards-footer-height; } } - .form-no-title #form-title { - display: none; + #form-title { + font-size: @font-XXL; + font-weight: normal; + letter-spacing: normal; + border: none; + background: @form-background-color; + width: 100%; } .form-footer { - padding: 24px 0; + position: absolute; + bottom: 0; + margin: 0; + background: @form-background-color; + padding: 20px 0; + z-index: 20; .btn, .btn:active, .btn:focus { @@ -121,16 +134,24 @@ mm-modal-layout { } } } + + &:not(.has-error) .enketo-modal .modal-footer { + padding: 0; + } + + &.has-error .enketo-modal .modal-body { + padding-top: 40px; + } + + .enketo-modal training-cards-confirm .modal-footer { + margin-bottom: 20px; + } } @media (max-width: @media-mobile) { mm-modal-layout { .enketo-modal { .enketo { - form { - max-height: 60vh; - } - .form-footer { .btn.btn-link.cancel { display: inline-block; @@ -142,13 +163,9 @@ mm-modal-layout { } } - // The form title is hidden by default for mobile devices, but in training cards, the title is optional. - :not(.form-no-title) #form-title { - display: inherit; - } - - .form-no-title #form-title { - display: none; + // The form's title is hidden by default for mobile devices; but in training cards, the title is displayed. + #form-title { + display: inline-block; } } } diff --git a/webapp/src/css/old-nav.less b/webapp/src/css/old-nav.less index c2d2dae7864..87c928d0f30 100644 --- a/webapp/src/css/old-nav.less +++ b/webapp/src/css/old-nav.less @@ -8,6 +8,18 @@ padding-top: 0; } + &.trainings { + .content .inner .page { + padding-top: 0; + padding-left: 0; + top: @top-without-filter; + } + + .tool-bar .inner { + background-color: @top-header-color; + } + } + .app-menu-button { display: none; } @@ -435,6 +447,10 @@ .targets .target:last-child { margin-bottom: 10px; } + + &.trainings.show-content .content .inner .page { + top: @toolbar-mobile-height; + } } #target-aggregates-list { diff --git a/webapp/src/css/variables.less b/webapp/src/css/variables.less index 3be3de65763..7453356dc76 100644 --- a/webapp/src/css/variables.less +++ b/webapp/src/css/variables.less @@ -68,6 +68,8 @@ @contacts-highlight: @pink-highlight; @reports-color: @yellow; @reports-highlight: @yellow-highlight; +@training-color: @gray-medium; +@training-highlight: @gray-ultra-lighter; @analytics-color: @teal; @analytics-highlight: @teal-highlight; @admin-color: @gray-dark; @@ -78,6 +80,7 @@ @pending-state-color: @yellow; @success-state-color: @teal-dark; @sent-state-color: @teal-dark; +@completed-state-color: @teal-dark; @muted-state-color: @gray-medium; @failed-state-color: @red; @cleared-state-color: @gray-medium; @@ -122,6 +125,8 @@ @tab-navbar-size: 80px; @more-options-icon-size-mobile: 32px; @more-options-icon-size-desktop: 24px; +@training-cards-footer-height: 95px; +@training-cards-page-height: 54vh; /* fonts */ @font-family-main: Noto, sans-serif; diff --git a/webapp/src/img/icon-check.svg b/webapp/src/img/icon-check.svg new file mode 100644 index 00000000000..cd4d2dece43 --- /dev/null +++ b/webapp/src/img/icon-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/webapp/src/ts/actions/global.ts b/webapp/src/ts/actions/global.ts index c058d2e395f..0e1472723e5 100644 --- a/webapp/src/ts/actions/global.ts +++ b/webapp/src/ts/actions/global.ts @@ -11,6 +11,7 @@ export const Actions = { setLoadingContent: createSingleValueAction('SET_LOADING_CONTENT', 'loadingContent'), setShowContent: createSingleValueAction('SET_SHOW_CONTENT', 'showContent'), setForms: createSingleValueAction('SET_FORMS', 'forms'), + setTrainingMaterials: createSingleValueAction('SET_TRAINING_MATERIALS', 'trainingMaterials'), clearFilters: createSingleValueAction('CLEAR_FILTERS', 'skip'), setFilter: createSingleValueAction('SET_FILTER', 'filter'), setSidebarFilter: createSingleValueAction('SET_SIDEBAR_FILTER', 'sidebarFilter'), @@ -38,6 +39,7 @@ export const Actions = { openSidebarMenu: createAction('OPEN_SIDEBAR_MENU'), setSearchBar: createSingleValueAction('SET_SEARCH_BAR', 'searchBar'), setTrainingCard: createSingleValueAction('SET_TRAINING_CARD', 'trainingCard'), + clearTrainingCards: createAction('CLEAR_TRAINING_CARDS'), }; export class GlobalActions { @@ -71,6 +73,10 @@ export class GlobalActions { return this.store.dispatch(Actions.setForms(forms)); } + setTrainingMaterials(trainingMaterials) { + return this.store.dispatch(Actions.setTrainingMaterials(trainingMaterials)); + } + setShowContent(showContent) { return this.store.dispatch(Actions.setShowContent(showContent)); } @@ -100,6 +106,15 @@ export class GlobalActions { return this.store.dispatch(Actions.setTrainingCard(trainingCard)); } + clearTrainingCards() { + this.setTrainingCard({ + formId: null, + isOpen: false, + showConfirmExit: false, + nextUrl: null, + }); + } + clearSidebarFilter() { return this.store.dispatch(Actions.clearSidebarFilter()); } diff --git a/webapp/src/ts/app-routing.module.ts b/webapp/src/ts/app-routing.module.ts index 3ecbaafc0cd..9ba91864672 100644 --- a/webapp/src/ts/app-routing.module.ts +++ b/webapp/src/ts/app-routing.module.ts @@ -11,6 +11,7 @@ import { routes as messagesRoutes } from '@mm-modules/messages/messages.routes'; import { routes as contactsRoutes } from '@mm-modules/contacts/contacts.routes'; import { routes as privacyPolicyRoutes } from '@mm-modules/privacy-policy/privacy-policy.routes'; import { routes as tasksRoutes } from '@mm-modules/tasks/tasks.routes'; +import { routes as trainingRoutes } from '@mm-modules/trainings/trainings.routes'; import { routes as testingRoutes } from '@mm-modules/testing/testing.routes'; const routes: Routes = [ @@ -23,6 +24,7 @@ const routes: Routes = [ ...contactsRoutes, ...privacyPolicyRoutes, ...tasksRoutes, + ...trainingRoutes, ...testingRoutes, ...errorRoutes, ]; diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts index 6ee6738633a..a0079893d52 100644 --- a/webapp/src/ts/app.component.ts +++ b/webapp/src/ts/app.component.ts @@ -83,7 +83,6 @@ export class AppComponent implements OnInit, AfterViewInit { private analyticsActions: AnalyticsActions; setupPromise; translationsLoaded; - currentTab = ''; privacyPolicyAccepted; isSidebarFilterOpen = false; @@ -94,14 +93,13 @@ export class AppComponent implements OnInit, AfterViewInit { canLogOut; replicationStatus; androidAppVersion; - unreadCount = {}; hasOldNav = false; initialisationComplete = false; - trainingCardFormId = ''; private readonly SVG_ICONS = new Map([ ['icon-close', './img/icon-close.svg'], ['icon-filter', './img/icon-filter.svg'], ['icon-back', './img/icon-back.svg'], + ['icon-check', './img/icon-check.svg'], ]); constructor ( @@ -482,16 +480,9 @@ export class AppComponent implements OnInit, AfterViewInit { combineLatest([ this.store.select(Selectors.getPrivacyPolicyAccepted), this.store.select(Selectors.getShowPrivacyPolicy), - this.store.select(Selectors.getTrainingCardFormId), - ]).subscribe(([ - privacyPolicyAccepted, - showPrivacyPolicy, - trainingCardFormId, - ]) => { + ]).subscribe(([ privacyPolicyAccepted, showPrivacyPolicy ]) => { this.showPrivacyPolicy = showPrivacyPolicy; this.privacyPolicyAccepted = privacyPolicyAccepted; - this.trainingCardFormId = trainingCardFormId || ''; - this.displayTrainingCards(); }); combineLatest([ @@ -502,14 +493,6 @@ export class AppComponent implements OnInit, AfterViewInit { }); } - private displayTrainingCards() { - if (!this.trainingCardFormId || (this.showPrivacyPolicy && !this.privacyPolicyAccepted)) { - return; - } - - this.trainingCardsService.displayTrainingCards(); - } - private async subscribeToSideFilterStore() { this.store .select(Selectors.getSidebarFilter) diff --git a/webapp/src/ts/components/components.module.ts b/webapp/src/ts/components/components.module.ts index f983ff0dd06..0c930b97e08 100644 --- a/webapp/src/ts/components/components.module.ts +++ b/webapp/src/ts/components/components.module.ts @@ -46,6 +46,7 @@ import { ModalLayoutComponent } from '@mm-components/modal-layout/modal-layout.c import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.component'; import { SidebarMenuComponent } from '@mm-components/sidebar-menu/sidebar-menu.component'; import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; +import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/training-cards-form.component'; @NgModule({ declarations: [ @@ -75,6 +76,7 @@ import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; ModalLayoutComponent, PanelHeaderComponent, SidebarMenuComponent, + TrainingCardsFormComponent, ToolBarComponent, ], imports: [ @@ -118,6 +120,7 @@ import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component'; ModalLayoutComponent, PanelHeaderComponent, SidebarMenuComponent, + TrainingCardsFormComponent, ToolBarComponent, ] }) diff --git a/webapp/src/ts/components/header/header.component.html b/webapp/src/ts/components/header/header.component.html index be5bd14f2f2..cd347ff4880 100644 --- a/webapp/src/ts/components/header/header.component.html +++ b/webapp/src/ts/components/header/header.component.html @@ -75,6 +75,12 @@ +
  • + + + {{'training_materials.page.title' | translate}} + +
  • diff --git a/webapp/src/ts/components/modal-layout/modal-layout.component.html b/webapp/src/ts/components/modal-layout/modal-layout.component.html index 105d9cc16bb..40e97d26b33 100644 --- a/webapp/src/ts/components/modal-layout/modal-layout.component.html +++ b/webapp/src/ts/components/modal-layout/modal-layout.component.html @@ -1,5 +1,5 @@