diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7305b4457..2133ea8ae8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: env: FHIRCORE_USERNAME: ${{ secrets.FHIRCORE_USERNAME }} FHIRCORE_ACCESS_TOKEN: ${{ secrets.FHIRCORE_ACCESS_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: - name: Checkout 🛎️ diff --git a/CHANGELOG.md b/CHANGELOG.md index b516c18f7b..469aef0b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added class for Measure report evaluation which will be used in ANC application - ANC | Added Condition resource to sync params list - Moved Token to secure storage from AccountManager +- QUEST | Patient List, Load Config from server +- QUEST | Added Patient Profile View ### Fixed diff --git a/android/anc/src/main/assets/sample_family_member_registration.json b/android/anc/src/main/assets/sample_family_member_registration.json index af9b734b4d..050d7394dc 100644 --- a/android/anc/src/main/assets/sample_family_member_registration.json +++ b/android/anc/src/main/assets/sample_family_member_registration.json @@ -32,7 +32,7 @@ { "linkId": "PR-name", "type": "group", - "text": "Family Head Information", + "text": "Demographic Information", "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.name", "item": [ { diff --git a/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivity.kt b/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivity.kt index 3eaa7e9a6c..4872a54380 100644 --- a/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivity.kt +++ b/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivity.kt @@ -76,7 +76,7 @@ class AncRegisterActivity : BaseRegisterActivity() { ) ) - override fun onSideMenuOptionSelected(item: MenuItem): Boolean { + override fun onMenuOptionSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_item_family -> startActivity(Intent(this, FamilyRegisterActivity::class.java)) R.id.menu_item_anc -> startActivity(Intent(this, AncRegisterActivity::class.java)) diff --git a/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/family/register/FamilyRegisterActivity.kt b/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/family/register/FamilyRegisterActivity.kt index 7e2b4079dc..ba47b59e87 100644 --- a/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/family/register/FamilyRegisterActivity.kt +++ b/android/anc/src/main/java/org/smartregister/fhircore/anc/ui/family/register/FamilyRegisterActivity.kt @@ -71,7 +71,7 @@ class FamilyRegisterActivity : BaseRegisterActivity() { ) ) - override fun onSideMenuOptionSelected(item: MenuItem): Boolean { + override fun onMenuOptionSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_item_family -> startActivity(Intent(this, FamilyRegisterActivity::class.java)) R.id.menu_item_anc -> startActivity(Intent(this, AncRegisterActivity::class.java)) diff --git a/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivityTest.kt b/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivityTest.kt index e88637c070..d5392650e0 100644 --- a/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivityTest.kt +++ b/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/anccare/register/AncRegisterActivityTest.kt @@ -118,7 +118,7 @@ internal class AncRegisterActivityTest : ActivityRobolectricTest() { fun testOnSideMenuOptionSelectedShouldVerifyActivityStarting() { val menuItemFamily = RoboMenuItem(R.id.menu_item_family) - ancRegisterActivity.onSideMenuOptionSelected(menuItemFamily) + ancRegisterActivity.onMenuOptionSelected(menuItemFamily) var expectedIntent = Intent(ancRegisterActivity, FamilyRegisterActivity::class.java) var actualIntent = @@ -127,7 +127,7 @@ internal class AncRegisterActivityTest : ActivityRobolectricTest() { assertEquals(expectedIntent.component, actualIntent.component) val menuItemAnc = RoboMenuItem(R.id.menu_item_anc) - ancRegisterActivity.onSideMenuOptionSelected(menuItemAnc) + ancRegisterActivity.onMenuOptionSelected(menuItemAnc) expectedIntent = Intent(ancRegisterActivity, AncRegisterActivity::class.java) actualIntent = diff --git a/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/family/FamilyRegisterActivityTest.kt b/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/family/FamilyRegisterActivityTest.kt index 54de4ad46b..5b6b144dde 100644 --- a/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/family/FamilyRegisterActivityTest.kt +++ b/android/anc/src/test/java/org/smartregister/fhircore/anc/ui/family/FamilyRegisterActivityTest.kt @@ -124,7 +124,7 @@ internal class FamilyRegisterActivityTest : ActivityRobolectricTest() { fun testOnSideMenuOptionSelectedShouldVerifyActivityStarting() { val menuItemFamily = RoboMenuItem(R.id.menu_item_family) - familyRegisterActivity.onSideMenuOptionSelected(menuItemFamily) + familyRegisterActivity.onMenuOptionSelected(menuItemFamily) var expectedIntent = Intent(familyRegisterActivity, FamilyRegisterActivity::class.java) var actualIntent = @@ -134,7 +134,7 @@ internal class FamilyRegisterActivityTest : ActivityRobolectricTest() { assertEquals(expectedIntent.component, actualIntent.component) val menuItemAnc = RoboMenuItem(R.id.menu_item_anc) - familyRegisterActivity.onSideMenuOptionSelected(menuItemAnc) + familyRegisterActivity.onMenuOptionSelected(menuItemAnc) expectedIntent = Intent(familyRegisterActivity, AncRegisterActivity::class.java) actualIntent = diff --git a/android/eir/src/main/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivity.kt b/android/eir/src/main/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivity.kt index 64d237a505..d74b3e593f 100644 --- a/android/eir/src/main/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivity.kt +++ b/android/eir/src/main/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivity.kt @@ -47,7 +47,7 @@ class PatientRegisterActivity : BaseRegisterActivity() { ) ) - override fun onSideMenuOptionSelected(item: MenuItem): Boolean { + override fun onMenuOptionSelected(item: MenuItem): Boolean { return true } diff --git a/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivityTest.kt b/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivityTest.kt index a4a80f0dd7..c1709d347a 100644 --- a/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivityTest.kt +++ b/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/patient/register/PatientRegisterActivityTest.kt @@ -18,9 +18,14 @@ package org.smartregister.fhircore.eir.ui.patient.register import android.app.Activity import androidx.core.content.ContextCompat +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert import org.junit.Before import org.junit.BeforeClass +import org.junit.Rule import org.junit.Test import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf @@ -28,14 +33,17 @@ import org.robolectric.annotation.Config import org.robolectric.fakes.RoboMenuItem import org.smartregister.fhircore.eir.R import org.smartregister.fhircore.eir.activity.ActivityRobolectricTest +import org.smartregister.fhircore.eir.coroutine.CoroutineTestRule import org.smartregister.fhircore.eir.shadow.EirApplicationShadow import org.smartregister.fhircore.eir.shadow.FakeKeyStore import org.smartregister.fhircore.eir.shadow.ShadowNpmPackageProvider +import org.smartregister.fhircore.engine.ui.register.model.SideMenuOption @Config(shadows = [EirApplicationShadow::class, ShadowNpmPackageProvider::class]) class PatientRegisterActivityTest : ActivityRobolectricTest() { private lateinit var patientRegisterActivity: PatientRegisterActivity + @get:Rule var coroutinesTestRule = CoroutineTestRule() @Before fun setUp() { @@ -62,7 +70,22 @@ class PatientRegisterActivityTest : ActivityRobolectricTest() { @Test fun testOnSideMenuOptionSelectedShouldReturnTrue() { - Assert.assertTrue(patientRegisterActivity.onSideMenuOptionSelected(RoboMenuItem())) + Assert.assertTrue(patientRegisterActivity.onMenuOptionSelected(RoboMenuItem())) + } + + @Test + fun testUpdateCountShouldSetRightValue() = runBlockingTest { + val spy = spyk(patientRegisterActivity) + every { spy.findSideMenuItem(any()) } returns + RoboMenuItem().apply { itemId = R.id.menu_item_covax } + + val sideMenuOption = + SideMenuOption(R.id.menu_item_covax, R.string.covax_app, mockk(), countMethod = { 123 }) + sideMenuOption.count = 0 + + spy.updateCount(sideMenuOption) + + Assert.assertEquals(123, sideMenuOption.count) } @Test diff --git a/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/vaccine/RecordVaccineActivityTest.kt b/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/vaccine/RecordVaccineActivityTest.kt index 6c8bbd69bc..749877cc3c 100644 --- a/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/vaccine/RecordVaccineActivityTest.kt +++ b/android/eir/src/test/java/org/smartregister/fhircore/eir/ui/vaccine/RecordVaccineActivityTest.kt @@ -95,34 +95,32 @@ class RecordVaccineActivityTest : ActivityRobolectricTest() { } @Test - fun testVerifyRecordedVaccineSavedDialogProperty() { - coroutinesTestRule.runBlockingTest { - val spyViewModel = - spyk((recordVaccineActivity.questionnaireViewModel as RecordVaccineViewModel)) - recordVaccineActivity.questionnaireViewModel = spyViewModel - - val callback = slot>() - - coEvery { spyViewModel.performExtraction(any(), any(), any()) } returns - Bundle().apply { addEntry().apply { resource = getImmunization() } } - - every { spyViewModel.getVaccineSummary(any()) } returns - mockk>().apply { - every { observe(any(), capture(callback)) } answers - { - callback.captured.onChanged(PatientVaccineSummary(1, "vaccine")) - } - } + fun testVerifyRecordedVaccineSavedDialogProperty() = runBlockingTest { + val spyViewModel = + spyk((recordVaccineActivity.questionnaireViewModel as RecordVaccineViewModel)) + recordVaccineActivity.questionnaireViewModel = spyViewModel - Assert.assertNull(ShadowAlertDialog.getLatestAlertDialog()) - recordVaccineActivity.handleQuestionnaireResponse(mockk()) + val callback = slot>() - val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) + coEvery { spyViewModel.performExtraction(any(), any(), any()) } returns + Bundle().apply { addEntry().apply { resource = getImmunization() } } - Assert.assertNotNull(dialog) - Assert.assertEquals("Initially received vaccine", dialog.title) - Assert.assertEquals("Second vaccine dose should be same as first", dialog.message) - } + every { spyViewModel.getVaccineSummary(any()) } returns + mockk>().apply { + every { observe(any(), capture(callback)) } answers + { + callback.captured.onChanged(PatientVaccineSummary(1, "vaccine")) + } + } + + Assert.assertNull(ShadowAlertDialog.getLatestAlertDialog()) + recordVaccineActivity.handleQuestionnaireResponse(mockk()) + + val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) + + Assert.assertNotNull(dialog) + Assert.assertEquals("Initially received vaccine", dialog.title) + Assert.assertEquals("Second vaccine dose should be same as first", dialog.message) } @Test diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 7cd60afbc0..cec8ec9c9c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -16,12 +16,18 @@ package org.smartregister.fhircore.engine.configuration.app +import android.content.Context +import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable +import org.smartregister.fhircore.engine.util.extension.decodeJson +import org.smartregister.fhircore.engine.util.extension.loadBinaryResourceConfiguration @Serializable data class ApplicationConfiguration( - var oauthServerBaseUrl: String, - var fhirServerBaseUrl: String, + var id: String = "", + var theme: String = "", + var oauthServerBaseUrl: String = "", + var fhirServerBaseUrl: String = "", var clientId: String = "", var clientSecret: String = "", var scope: String = "openid", @@ -44,6 +50,8 @@ data class ApplicationConfiguration( * @param syncInterval Sets the periodic sync interval in seconds. Default 30. */ fun applicationConfigurationOf( + id: String = "", + theme: String = "", oauthServerBaseUrl: String = "", fhirServerBaseUrl: String = "", clientId: String = "", @@ -53,6 +61,8 @@ fun applicationConfigurationOf( syncInterval: Long = 30 ): ApplicationConfiguration = ApplicationConfiguration( + id = id, + theme = theme, oauthServerBaseUrl = oauthServerBaseUrl, fhirServerBaseUrl = fhirServerBaseUrl, clientId = clientId, @@ -61,3 +71,15 @@ fun applicationConfigurationOf( languages = languages, syncInterval = syncInterval ) + +private const val APPLICATION_CONFIG_FILE = "application_config.json" + +fun Context.loadApplicationConfiguration(id: String): ApplicationConfiguration { + return runBlocking { loadBinaryResourceConfiguration(id) } + ?: assets + .open(APPLICATION_CONFIG_FILE) + .bufferedReader() + .use { it.readText() } + .decodeJson>() + .first { it.id == id } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/RegisterViewConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/RegisterViewConfiguration.kt index 46978a8ef7..bc9d60464d 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/RegisterViewConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/RegisterViewConfiguration.kt @@ -18,23 +18,30 @@ package org.smartregister.fhircore.engine.configuration.view import android.content.Context import androidx.compose.runtime.Stable +import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.Configuration +import org.smartregister.fhircore.engine.util.extension.decodeJson +import org.smartregister.fhircore.engine.util.extension.loadBinaryResourceConfiguration @Serializable @Stable data class RegisterViewConfiguration( + var id: String, var appTitle: String, var filterText: String, var searchBarHint: String, var newClientButtonText: String, + var newClientButtonStyle: String, var showSearchBar: Boolean = true, var showFilter: Boolean = true, var switchLanguages: Boolean = true, var showScanQRCode: Boolean = true, var showNewClientButton: Boolean = true, var registrationForm: String = "patient-registration", + var showSideMenu: Boolean = true, + var showBottomMenu: Boolean = false ) : Configuration /** @@ -54,28 +61,48 @@ data class RegisterViewConfiguration( */ @Stable fun Context.registerViewConfigurationOf( + id: String = this.getString(R.string.default_app_title), appTitle: String = this.getString(R.string.default_app_title), filterText: String = this.getString(R.string.show_overdue), searchBarHint: String = this.getString(R.string.search_hint), newClientButtonText: String = this.getString(R.string.register_new_client), + newClientButtonStyle: String = "", showSearchBar: Boolean = true, showFilter: Boolean = true, switchLanguages: Boolean = true, showScanQRCode: Boolean = true, showNewClientButton: Boolean = true, languages: List = listOf("en"), - registrationForm: String = "patient-registration" + registrationForm: String = "patient-registration", + showSideMenu: Boolean = true, + showBottomMenu: Boolean = false ): RegisterViewConfiguration { return RegisterViewConfiguration( + id = id, appTitle = appTitle, filterText = filterText, searchBarHint = searchBarHint, newClientButtonText = newClientButtonText, + newClientButtonStyle = newClientButtonStyle, showSearchBar = showSearchBar, showFilter = showFilter, switchLanguages = switchLanguages, showScanQRCode = showScanQRCode, showNewClientButton = showNewClientButton, registrationForm = registrationForm, + showSideMenu = showSideMenu, + showBottomMenu = showBottomMenu ) } + +private const val REGISTER_VIEW_CONFIG_FILE = "register_view_config.json" + +fun Context.loadRegisterViewConfiguration(id: String): RegisterViewConfiguration { + return runBlocking { loadBinaryResourceConfiguration(id) } + ?: assets + .open(REGISTER_VIEW_CONFIG_FILE) + .bufferedReader() + .use { it.readText() } + .decodeJson>() + .first { it.id == id } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt index b27917cdf0..1b40ed50ca 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/BaseMultiLanguageActivity.kt @@ -17,12 +17,20 @@ package org.smartregister.fhircore.engine.ui.base import android.content.Context +import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import java.util.Locale import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.extension.getTheme import org.smartregister.fhircore.engine.util.extension.setAppLocale abstract class BaseMultiLanguageActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val themePref = SharedPreferencesHelper.read(SharedPreferencesHelper.THEME, "")!! + theme.applyStyle(getTheme(themePref), true) + } override fun attachBaseContext(baseContext: Context) { val lang = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/BaseRegisterActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/BaseRegisterActivity.kt index e028a32ff8..9212818594 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/BaseRegisterActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/BaseRegisterActivity.kt @@ -28,6 +28,7 @@ import android.view.MenuItem import android.view.View import android.widget.ArrayAdapter import android.widget.TextView +import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat @@ -56,6 +57,7 @@ import org.smartregister.fhircore.engine.sync.SyncInitiator import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity import org.smartregister.fhircore.engine.ui.register.model.Language +import org.smartregister.fhircore.engine.ui.register.model.NavigationMenuOption import org.smartregister.fhircore.engine.ui.register.model.RegisterFilterType import org.smartregister.fhircore.engine.ui.register.model.SideMenuOption import org.smartregister.fhircore.engine.util.DateUtils @@ -65,7 +67,6 @@ import org.smartregister.fhircore.engine.util.extension.DrawablePosition import org.smartregister.fhircore.engine.util.extension.addOnDrawableClickListener import org.smartregister.fhircore.engine.util.extension.asString import org.smartregister.fhircore.engine.util.extension.assertIsConfigurable -import org.smartregister.fhircore.engine.util.extension.countActivePatients import org.smartregister.fhircore.engine.util.extension.createFactory import org.smartregister.fhircore.engine.util.extension.getDrawable import org.smartregister.fhircore.engine.util.extension.hide @@ -87,8 +88,6 @@ abstract class BaseRegisterActivity : private lateinit var registerActivityBinding: BaseRegisterActivityBinding - private lateinit var drawerMenuHeaderBinding: DrawerMenuHeaderBinding - override val configurableViews: Map = mutableMapOf() private var mainRegisterSideMenuOption: SideMenuOption? = null @@ -143,12 +142,7 @@ abstract class BaseRegisterActivity : registerActivityBinding = DataBindingUtil.setContentView(this, R.layout.base_register_activity) registerActivityBinding.lifecycleOwner = this - drawerMenuHeaderBinding = - DataBindingUtil.bind(registerActivityBinding.navView.getHeaderView(0))!! - fhirEngine = (application as ConfigurableApplication).fhirEngine - - setUpViews() } override fun onResume() { @@ -196,25 +190,22 @@ abstract class BaseRegisterActivity : } private fun setUpViews() { - setupSideMenu() + setupDrawer(registerViewModel.registerViewConfiguration.value!!) + with(registerActivityBinding) { toolbarLayout.btnDrawerMenu.setOnClickListener { manipulateDrawer(open = true) } btnRegisterNewClient.setOnClickListener { registerClient() } containerProgressSync.setOnClickListener { syncButtonClick() } } - registerActivityBinding.navView.apply { - setNavigationItemSelectedListener(this@BaseRegisterActivity) - menu.findItem(R.id.menu_item_logout).title = - getString( - R.string.logout_user, - configurableApplication().secureSharedPreference.retrieveSessionUsername() - ) - } // Setup view pager registerPagerAdapter = RegisterPagerAdapter(this, supportedFragments = supportedFragments()) registerActivityBinding.listPager.adapter = registerPagerAdapter + setupNewClientButtonView(registerViewModel.registerViewConfiguration.value!!) + + updateRegisterTitle() + setupSearchView() setupDueButtonView() } @@ -291,6 +282,16 @@ abstract class BaseRegisterActivity : } } + private fun setupNewClientButtonView(registerViewConfiguration: RegisterViewConfiguration) { + with(registerActivityBinding.btnRegisterNewClient) { + this.setOnClickListener { registerClient() } + if (registerViewConfiguration.newClientButtonStyle.isNotEmpty()) { + this.background = getDrawable(registerViewConfiguration.newClientButtonStyle) + } + this.text = registerViewConfiguration.newClientButtonText + } + } + private fun setupDueButtonView() { with(registerActivityBinding.toolbarLayout) { btnShowOverdue.setOnCheckedChangeListener { _, isChecked -> @@ -303,6 +304,35 @@ abstract class BaseRegisterActivity : } } + private fun setupDrawer(viewConfiguration: RegisterViewConfiguration) { + if (!viewConfiguration.showSideMenu) { + with(registerActivityBinding) { + toolbarLayout.btnDrawerMenu.hide(true) + // TODO uncomment this when sync issue is resolved + // drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + return + } + + val drawerMenuHeaderBinding: DrawerMenuHeaderBinding = + DataBindingUtil.bind(registerActivityBinding.navView.getHeaderView(0))!! + drawerMenuHeaderBinding.tvNavHeader.text = viewConfiguration.appTitle + + setupSideMenu() + + val languageMenuItem = findSideMenuItem(R.id.menu_item_language)!! + languageMenuItem.isVisible = viewConfiguration.switchLanguages + + registerActivityBinding.navView.apply { + setNavigationItemSelectedListener(this@BaseRegisterActivity) + menu.findItem(R.id.menu_item_logout)?.title = + getString( + R.string.logout_user, + configurableApplication().secureSharedPreference.retrieveSessionUsername() + ) + } + } + private fun setupSideMenu() { sideMenuOptionMap = sideMenuOptions().associateBy { it.itemId } sideMenuOptionMap.values.firstOrNull { it.opensMainRegister }?.let { @@ -338,8 +368,6 @@ abstract class BaseRegisterActivity : 2, "" ) // Hack to add last menu divider - - updateRegisterTitle() } private fun manipulateDrawer(open: Boolean = false) { @@ -386,11 +414,7 @@ abstract class BaseRegisterActivity : } override fun setupConfigurableViews(viewConfiguration: RegisterViewConfiguration) { - drawerMenuHeaderBinding.tvNavHeader.text = viewConfiguration.appTitle - val navView = registerActivityBinding.navView - - val languageMenuItem = navView.menu.findItem(R.id.menu_item_language) - languageMenuItem.isVisible = viewConfiguration.switchLanguages + setUpViews() with(registerActivityBinding.toolbarLayout) { btnShowOverdue.apply { @@ -403,6 +427,17 @@ abstract class BaseRegisterActivity : } layoutScanBarcode.toggleVisibility(viewConfiguration.showScanQRCode) } + + setupBottomNavigationMenu(viewConfiguration) + } + + open fun setupBottomNavigationMenu(viewConfiguration: RegisterViewConfiguration) { + val bottomMenu = registerActivityBinding.bottomNavView.menu + if (!viewConfiguration.showBottomMenu) registerActivityBinding.bottomNavView.hide(true) + + bottomNavigationMenuOptions().forEach { + bottomMenu.add(it.title).apply { it.iconResource?.let { ic -> this.icon = ic } } + } } override fun onNavigationItemSelected(item: MenuItem): Boolean { @@ -421,7 +456,7 @@ abstract class BaseRegisterActivity : } else -> { manipulateDrawer(open = false) - return onSideMenuOptionSelected(item) + return onMenuOptionSelected(item) } } return true @@ -430,6 +465,7 @@ abstract class BaseRegisterActivity : private fun updateRegisterTitle() { registerActivityBinding.toolbarLayout.tvClientsListTitle.text = selectedMenuOption?.titleResource?.let { getString(it) } + ?: registerViewModel.registerViewConfiguration.value?.appTitle } private fun renderSelectLanguageDialog(context: Activity): AlertDialog { @@ -466,18 +502,31 @@ abstract class BaseRegisterActivity : } private fun updateLanguage(language: Language) { - (registerActivityBinding.navView.menu.findItem(R.id.menu_item_language).actionView as TextView) - .text = language.displayName + findSideMenuItem(R.id.menu_item_language)?.let { + (it.actionView as TextView).text = language.displayName + } + } + + fun findSideMenuItem(@IdRes id: Int): MenuItem? { + return registerActivityBinding.navView.menu.findItem(id) } /** List of [SideMenuOption] representing individual menu items listed in the DrawerLayout */ - abstract fun sideMenuOptions(): List + open fun sideMenuOptions(): List { + return emptyList() + } + + /** List of [SideMenuOption] representing individual menu items listed in the DrawerLayout */ + open fun bottomNavigationMenuOptions(): List { + return emptyList() + } /** * Abstract method to be implemented by the subclass to provide actions for the menu [item] * options. Refer to the selected menu item using the view tag that was set by [sideMenuOptions] + * or [bottomNavigationMenuOptions] */ - abstract fun onSideMenuOptionSelected(item: MenuItem): Boolean + abstract fun onMenuOptionSelected(item: MenuItem): Boolean protected open fun registerClient() { startActivity( @@ -502,20 +551,16 @@ abstract class BaseRegisterActivity : return application as ConfigurableApplication } - private fun updateCount(sideMenuOption: SideMenuOption) { + fun updateCount(sideMenuOption: SideMenuOption) { lifecycleScope.launch(registerViewModel.dispatcher.main()) { val count = sideMenuOption.countMethod() - if (count == -1L) { - sideMenuOption.count = - (application as ConfigurableApplication).fhirEngine.countActivePatients() - } else { - sideMenuOption.count = count - } + with(sideMenuOption) { - val counter = - registerActivityBinding.navView.menu.findItem(sideMenuOption.itemId).actionView as - TextView - counter.text = if (this.count > 0) this.count.toString() else null + this.count = count + + findSideMenuItem(sideMenuOption.itemId)?.let { + (it.actionView as TextView).text = if (this.count > 0) this.count.toString() else "" + } if (selectedMenuOption != null && this.itemId == selectedMenuOption?.itemId) { selectedMenuOption?.count = this.count } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/model/NavigationMenuOption.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/model/NavigationMenuOption.kt new file mode 100644 index 0000000000..7b0809bc4a --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/register/model/NavigationMenuOption.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.ui.register.model + +import android.graphics.drawable.Drawable + +data class NavigationMenuOption(val id: Int, val title: String, val iconResource: Drawable) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt index ea6884b415..b6d7c58fe0 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt @@ -24,6 +24,7 @@ object SharedPreferencesHelper { private lateinit var prefs: SharedPreferences const val LANG = "shared_pref_lang" + const val THEME = "shared_pref_theme" private const val PREFS_NAME = "params" fun init(context: Context): SharedPreferencesHelper { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt index 49d20af7d6..7168eeb571 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt @@ -22,10 +22,14 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.content.res.Resources +import android.graphics.drawable.Drawable import android.os.Build import android.os.LocaleList import android.widget.Toast +import androidx.annotation.StyleRes +import androidx.core.content.ContextCompat import java.util.Locale +import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.app.ConfigurableApplication import timber.log.Timber @@ -55,6 +59,19 @@ fun Context.setAppLocale(languageTag: String): Configuration? { return configuration } +fun Context.getDrawable(name: String): Drawable { + var resourceId = this.resources.getIdentifier(name, "drawable", packageName) + if (resourceId == 0) resourceId = R.drawable.ic_default_logo + return ContextCompat.getDrawable(this, resourceId)!! +} + +@StyleRes +fun Context.getTheme(name: String): Int { + var resourceId = this.resources.getIdentifier(name, "style", packageName) + if (resourceId == 0) resourceId = R.style.AppTheme_NoActionBar + return resourceId +} + fun Application.assertIsConfigurable() { if (this !is ConfigurableApplication) throw (IllegalStateException("Application MUST implement ConfigurableApplication interface")) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt index 62a4e6f4d0..4ab4517a99 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ApplicationExtension.kt @@ -17,6 +17,7 @@ package org.smartregister.fhircore.engine.util.extension import android.app.Application +import android.content.Context import ca.uhn.fhir.context.FhirContext import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.utilities.SimpleWorkerContextProvider @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.hl7.fhir.r4.context.SimpleWorkerContext +import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Immunization import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.RelatedPerson @@ -94,6 +96,17 @@ fun Application.loadResourceTemplate( else Gson().fromJson(json, clazz) } +suspend inline fun Context.loadBinaryResourceConfiguration(id: String): T? { + val fhirEngine = (applicationContext as ConfigurableApplication).fhirEngine + return kotlin + .runCatching { + val binaryConfig = fhirEngine.load(Binary::class.java, id).content.decodeToString() + binaryConfig.decodeJson() as T + } + .onFailure { Timber.w(it) } + .getOrNull() +} + suspend fun FhirEngine.searchActivePatients( query: String, pageNumber: Int, diff --git a/android/engine/src/main/res/drawable/rounded_corner.xml b/android/engine/src/main/res/drawable/rounded_corner.xml new file mode 100644 index 0000000000..1d7d28ec00 --- /dev/null +++ b/android/engine/src/main/res/drawable/rounded_corner.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/android/engine/src/main/res/layout/activity_questionnaire.xml b/android/engine/src/main/res/layout/activity_questionnaire.xml index 72a809b109..83163fe4f4 100644 --- a/android/engine/src/main/res/layout/activity_questionnaire.xml +++ b/android/engine/src/main/res/layout/activity_questionnaire.xml @@ -37,8 +37,7 @@ android:layout_marginBottom="15dp" android:layout_marginEnd="15dp" android:layout_marginStart="15dp" - android:background="@drawable/rounded_white_bg" - android:backgroundTint="@color/colorPrimary" + android:background="?attr/colorPrimary" android:drawablePadding="5dp" android:fontFamily="sans-serif-medium" android:padding="10dp" diff --git a/android/engine/src/main/res/layout/base_register_activity.xml b/android/engine/src/main/res/layout/base_register_activity.xml index 997729be55..0863c2367b 100644 --- a/android/engine/src/main/res/layout/base_register_activity.xml +++ b/android/engine/src/main/res/layout/base_register_activity.xml @@ -44,61 +44,21 @@ android:id="@+id/list_pager" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_above="@id/pagination_section" + android:layout_above="@id/btn_register_new_client" android:layout_below="@id/base_register_toolbar" - android:layout_marginBottom="15dp"> + android:layout_marginBottom="5dp"> - - -