diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/OpenSrpApplication.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/OpenSrpApplication.kt new file mode 100644 index 0000000000..f256f84c06 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/OpenSrpApplication.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021-2023 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 + +import android.app.Application + +abstract class OpenSrpApplication : Application() { + abstract fun getFhirServerHost(): String +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 0fa91ebf35..f4af386ad3 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -57,6 +57,8 @@ data class QuestionnaireConfig( val generateCarePlanWithWorkflowApi: Boolean = false, val cqlInputResources: List? = emptyList(), val showClearAll: Boolean = false, + val showRequiredTextAsterisk: Boolean = true, + val showRequiredText: Boolean = false, ) : java.io.Serializable, Parcelable { fun interpolate(computedValuesMap: Map) = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt index 46d7242343..5a43dd8a66 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.engine.di +import android.content.Context import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import com.google.gson.Gson @@ -24,6 +25,7 @@ import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFact import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -36,6 +38,8 @@ import okhttp3.Protocol import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.logging.HttpLoggingInterceptor +import org.smartregister.fhircore.engine.BuildConfig +import org.smartregister.fhircore.engine.OpenSrpApplication import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.auth.KeycloakService import org.smartregister.fhircore.engine.data.remote.auth.OAuthService @@ -51,6 +55,7 @@ import timber.log.Timber @InstallIn(SingletonComponent::class) @Module class NetworkModule { + private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK @Provides @NoAuthorizationOkHttpClientQualifier @@ -73,8 +78,39 @@ class NetworkModule { fun provideOkHttpClient( tokenAuthenticator: TokenAuthenticator, sharedPreferencesHelper: SharedPreferencesHelper, + openSrpApplication: OpenSrpApplication?, ) = OkHttpClient.Builder() + .addInterceptor( + Interceptor { chain: Interceptor.Chain -> + try { + var request = chain.request() + val requestPath = request.url.encodedPath.substring(1) + val resourcePath = if (!_isNonProxy) requestPath.replace("fhir/", "") else requestPath + + openSrpApplication?.let { + if ( + (request.url.host == it.getFhirServerHost()) && + CUSTOM_ENDPOINTS.contains(resourcePath) + ) { + val newUrl = request.url.newBuilder().encodedPath("/$resourcePath").build() + request = request.newBuilder().url(newUrl).build() + } + } + + chain.proceed(request) + } catch (e: Exception) { + Timber.e(e) + Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(901) + .message(e.message ?: "Failed to overwrite URL request successfully") + .body("{$e}".toResponseBody(null)) + .build() + } + }, + ) .addInterceptor( Interceptor { chain: Interceptor.Chain -> try { @@ -182,11 +218,17 @@ class NetworkModule { fun provideFhirResourceService(@RegularRetrofit retrofit: Retrofit): FhirResourceService = retrofit.create(FhirResourceService::class.java) + @Provides + @Singleton + fun provideFHIRBaseURL(@ApplicationContext context: Context): OpenSrpApplication? = + if (context is OpenSrpApplication) context else null + companion object { const val TIMEOUT_DURATION = 120L const val AUTHORIZATION = "Authorization" const val APPLICATION_ID = "App-Id" const val COOKIE = "Cookie" val JSON_MEDIA_TYPE = "application/json".toMediaType() + val CUSTOM_ENDPOINTS = listOf("PractitionerDetail", "LocationHierarchy") } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ServiceMemberIcon.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ServiceMemberIcon.kt index d738c8ae45..c03a7aa413 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ServiceMemberIcon.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ServiceMemberIcon.kt @@ -26,4 +26,12 @@ import org.smartregister.fhircore.engine.R enum class ServiceMemberIcon(val icon: Int) { @JsonNames("child", "Child") CHILD(R.drawable.ic_kids), @JsonNames("pregnant_woman", "PregnantWoman") PREGNANT_WOMAN(R.drawable.ic_pregnant), + @JsonNames("post_partum_mother", "PostPartumMother") + POST_PARTUM_MOTHER(R.drawable.ic_post_partum_mother), + @JsonNames("woman_of_reproductive_age", "WomanOfReproductiveAge") + WOMAN_OF_REPRODUCTIVE_AGE(R.drawable.ic_woman_of_reproductive_age), + @JsonNames("elderly", "Elderly") ELDERLY(R.drawable.ic_elderly), + @JsonNames("baby_boy", "BabyBoy") BABY_BOY(R.drawable.ic_baby_boy), + @JsonNames("baby_girl", "BabyGirl") BABY_GIRL(R.drawable.ic_baby_girl), + @JsonNames("sick_child", "SickChild") SICK_CHILD(R.drawable.ic_sick_child), } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt index 1831480faf..658a759554 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt @@ -103,7 +103,7 @@ fun String.camelCase(): String = CaseUtils.toCamelCase(this, false, '_') * Get the practitioner endpoint url and append the keycloak-uuid. The original String is assumed to * be a keycloak-uuid. */ -fun String.practitionerEndpointUrl(): String = "practitioner-details?keycloak-uuid=$this" +fun String.practitionerEndpointUrl(): String = "PractitionerDetail?keycloak-uuid=$this" /** Remove double white spaces from text and also remove space before comma */ fun String.removeExtraWhiteSpaces(): String = diff --git a/android/engine/src/main/res/drawable/ic_baby_boy.xml b/android/engine/src/main/res/drawable/ic_baby_boy.xml new file mode 100644 index 0000000000..b6859e3f4b --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_baby_boy.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/engine/src/main/res/drawable/ic_baby_girl.xml b/android/engine/src/main/res/drawable/ic_baby_girl.xml new file mode 100644 index 0000000000..50a3c07e87 --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_baby_girl.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/engine/src/main/res/drawable/ic_elderly.xml b/android/engine/src/main/res/drawable/ic_elderly.xml new file mode 100644 index 0000000000..3da7e8d6b5 --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_elderly.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/android/engine/src/main/res/drawable/ic_post_partum_mother.xml b/android/engine/src/main/res/drawable/ic_post_partum_mother.xml new file mode 100644 index 0000000000..f9e22747b7 --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_post_partum_mother.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/engine/src/main/res/drawable/ic_sick_child_new.xml b/android/engine/src/main/res/drawable/ic_sick_child_new.xml new file mode 100644 index 0000000000..14d371e616 --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_sick_child_new.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/android/engine/src/main/res/drawable/ic_woman_of_reproductive_age.xml b/android/engine/src/main/res/drawable/ic_woman_of_reproductive_age.xml new file mode 100644 index 0000000000..05a1d2e21a --- /dev/null +++ b/android/engine/src/main/res/drawable/ic_woman_of_reproductive_age.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt index fb8251d723..6ebefb9237 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/StringExtensionTest.kt @@ -24,7 +24,7 @@ class StringExtensionTest { @Test fun practitionerEndpointUrlShouldMatch() { Assert.assertEquals( - "practitioner-details?keycloak-uuid=my-keycloak-id", + "PractitionerDetail?keycloak-uuid=my-keycloak-id", "my-keycloak-id".practitionerEndpointUrl(), ) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index b0e53ea057..85061eeb37 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -24,7 +24,7 @@ data-capture = "1.0.0-preview19-SNAPSHOT" desugar-jdk-libs = "1.1.5" easy-rules-jexl = "4.1.0" espresso-core = "3.5.1" -fhir-common-utils = "0.0.10-SNAPSHOT" +fhir-common-utils = "1.0.0-SNAPSHOT" fhir-engine = "0.1.0-beta04-preview4.1-SNAPSHOT" foundation = "1.3.1" fragment-ktx = "1.6.1" diff --git a/android/properties.gradle.kts b/android/properties.gradle.kts index 62c19ebb40..29487e49a7 100644 --- a/android/properties.gradle.kts +++ b/android/properties.gradle.kts @@ -26,12 +26,19 @@ val requiredFhirProperties = "OAUTH_CLIENT_ID", "OAUTH_SCOPE", "MAPBOX_SDK_TOKEN", - "SENTRY_DSN" + "SENTRY_DSN", + "OPENSRP_APP_ID" ) val localProperties = readProperties((project.properties["localPropertiesFile"] ?: "local.properties").toString()) + requiredFhirProperties.forEach { property -> - project.extra.set(property, localProperties.getProperty(property,if(property.contains("URL")) "https://sample.url/fhir/" else "sample_" + property)) + project.extra.set(property, localProperties.getProperty(property, when { + property.contains("URL") -> "https://sample.url/fhir/" + property.equals("OPENSRP_APP_ID") -> """""""" + else -> "sample_" + property + } + )) } // Set required keystore properties diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index ae7203709f..a393a17073 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -65,6 +65,7 @@ android { buildConfigField("String", "OAUTH_BASE_URL", """"${project.extra["OAUTH_BASE_URL"]}"""") buildConfigField("String", "OAUTH_CLIENT_ID", """"${project.extra["OAUTH_CLIENT_ID"]}"""") buildConfigField("String", "OAUTH_SCOPE", """"${project.extra["OAUTH_SCOPE"]}"""") + buildConfigField("String", "OPENSRP_APP_ID", """${project.extra["OPENSRP_APP_ID"]}""") buildConfigField("String", "CONFIGURATION_SYNC_PAGE_SIZE", """"100"""") buildConfigField("String", "SENTRY_DSN", """"${project.extra["SENTRY_DSN"]}"""") diff --git a/android/quest/src/main/assets/configs/app/profiles/household_profile_config.json b/android/quest/src/main/assets/configs/app/profiles/household_profile_config.json index 20a9a46702..b3518a19d5 100644 --- a/android/quest/src/main/assets/configs/app/profiles/household_profile_config.json +++ b/android/quest/src/main/assets/configs/app/profiles/household_profile_config.json @@ -384,8 +384,8 @@ "name": "serviceMemberIcons", "condition": "true", "actions": [ - "data.put('serviceMemberIcons', StringUtils:join([fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate >= today() - 5 'years')\") == 'true'? 'CHILD': '', service.mapResourcesToLabeledCSV(Condition, \"Condition.code.text = 'Pregnant'\", 'PREGNANT_WOMAN')], ','))" - ] + "data.put('serviceMemberIcons', StringUtils:join([fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate >= today() - 5 'years') and (Patient.gender= 'male')\") == 'true'? 'BABY_BOY': '', fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate >= today() - 5 'years') and (Patient.gender= 'female')\") == 'true'? 'BABY_GIRL': '', fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate <= today() - 20 'years') and (Patient.gender = 'female')\") == 'true'? 'WOMAN_OF_REPRODUCTIVE_AGE': '',fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate <= today() - 60 'years')\") == 'true'? 'ELDERLY': ''], ','))" + ] }, { "name": "relatedPersonCount", diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestApplication.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestApplication.kt index 6ba5f53240..d70e8da71f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestApplication.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestApplication.kt @@ -16,7 +16,6 @@ package org.smartregister.fhircore.quest -import android.app.Application import android.content.Intent import android.database.CursorWindow import android.os.Looper @@ -31,6 +30,7 @@ import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.fragment.FragmentLifecycleIntegration import java.net.URL import javax.inject.Inject +import org.smartregister.fhircore.engine.OpenSrpApplication import org.smartregister.fhircore.engine.data.remote.fhir.resource.ReferenceUrlResolver import org.smartregister.fhircore.engine.util.extension.getSubDomain import org.smartregister.fhircore.engine.util.extension.showToast @@ -40,7 +40,7 @@ import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireItemViewHo import timber.log.Timber @HiltAndroidApp -class QuestApplication : Application(), DataCaptureConfig.Provider, Configuration.Provider { +class QuestApplication : OpenSrpApplication(), DataCaptureConfig.Provider, Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var referenceUrlResolver: ReferenceUrlResolver @@ -48,6 +48,8 @@ class QuestApplication : Application(), DataCaptureConfig.Provider, Configuratio @Inject lateinit var xFhirQueryResolver: QuestXFhirQueryResolver private var configuration: DataCaptureConfig? = null + private var fhirServerHost: String? = null + override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { @@ -141,4 +143,9 @@ class QuestApplication : Application(), DataCaptureConfig.Provider, Configuratio startActivity(intent) } } + + override fun getFhirServerHost(): String { + fhirServerHost = fhirServerHost ?: URL(BuildConfig.FHIR_BASE_URL).host + return fhirServerHost ?: "" + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingActivity.kt index 7a1d990a72..06eeee1c64 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingActivity.kt @@ -37,6 +37,7 @@ import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.applyWindowInsetListener import org.smartregister.fhircore.engine.util.extension.showToast +import org.smartregister.fhircore.quest.BuildConfig import org.smartregister.fhircore.quest.ui.login.AccountAuthenticator @AndroidEntryPoint @@ -73,12 +74,17 @@ class AppSettingActivity : AppCompatActivity() { onApplicationIdChanged(existingAppId) loadConfigurations(appSettingActivity) } + } else if (!BuildConfig.OPENSRP_APP_ID.isNullOrEmpty()) { + // this part simulates what the user would have done manually via the text field and button + appSettingViewModel.onApplicationIdChanged(BuildConfig.OPENSRP_APP_ID) + appSettingViewModel.fetchConfigurations(appSettingActivity) } else { setContent { AppTheme { val appId by appSettingViewModel.appId.observeAsState("") val showProgressBar by appSettingViewModel.showProgressBar.observeAsState(false) val error by appSettingViewModel.error.observeAsState("") + AppSettingScreen( appId = appId, onAppIdChanged = appSettingViewModel::onApplicationIdChanged, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingScreen.kt index d58769ffa3..5cb6783277 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingScreen.kt @@ -160,6 +160,7 @@ fun AppSettingScreen( modifier = modifier.padding(8.dp), ) } + if (showProgressBar) { CircularProgressIndicator( modifier = modifier.align(Alignment.Center).size(18.dp), diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt index 854b1ba615..352aee1ff5 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt @@ -108,6 +108,7 @@ constructor( private fun fetchRemoteConfigurations(appId: String?, context: Context) { viewModelScope.launch { try { + showProgressBar.postValue(true) Timber.i("Fetching configs for app $appId") val urlPath = "${ResourceType.Composition.name}?${Composition.SP_IDENTIFIER}=$appId&_count=${ConfigurationRegistry.DEFAULT_COUNT}" diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 706ee75b93..0831bf6692 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -157,7 +157,10 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { finish() } val questionnaireFragmentBuilder = - QuestionnaireFragment.builder().setQuestionnaire(questionnaire.json()) + QuestionnaireFragment.builder() + .setQuestionnaire(questionnaire.json()) + .showAsterisk(questionnaireConfig.showRequiredTextAsterisk) + .showRequiredText(questionnaireConfig.showRequiredText) val questionnaireSubjectType = questionnaire.subjectType.firstOrNull()?.code val resourceType = diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt index 0c93d07d4c..d4417467be 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt @@ -74,6 +74,7 @@ import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.p2p.utils.capitalize const val DIVIDER_TEST_TAG = "dividerTestTag" +const val NUMBER_OF_ICONS_DISPLAYED = 2 @Composable fun ServiceCard( @@ -170,8 +171,8 @@ private fun RowScope.RenderDetails( navController: NavController, resourceData: ResourceData, ) { - val iconsSplit = serviceMemberIcons?.split(",") ?: listOf() - val twoMemberIcons = iconsSplit.map { it.capitalize().trim() }.take(2) + val iconsSplit = serviceMemberIcons?.split(",")?.filter { it.isNotEmpty() } ?: listOf() + val memberIcons = iconsSplit.map { it.capitalize().trim() }.take(NUMBER_OF_ICONS_DISPLAYED) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(weight).padding(end = 6.dp).fillMaxWidth(), @@ -190,14 +191,14 @@ private fun RowScope.RenderDetails( ) } } - // Display 2 icons and counter if icons are more than 2 - if (twoMemberIcons.isNotEmpty()) { + // Display N icons and counter if icons are more than N + if (memberIcons.isNotEmpty()) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.wrapContentWidth(), horizontalArrangement = Arrangement.End, ) { - twoMemberIcons.forEach { + memberIcons.forEach { if ( it.isNotEmpty() && ServiceMemberIcon.values().map { icon -> icon.name }.contains(it) ) { @@ -209,13 +210,16 @@ private fun RowScope.RenderDetails( ) } } - if (twoMemberIcons.size == 2 && iconsSplit.size > 2) { + if ( + memberIcons.size == NUMBER_OF_ICONS_DISPLAYED && + iconsSplit.size > NUMBER_OF_ICONS_DISPLAYED + ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.clip(CircleShape).size(22.dp).background(DefaultColor.copy(0.1f)), ) { Text( - text = "+${iconsSplit.size - 2}", + text = "+${iconsSplit.size - NUMBER_OF_ICONS_DISPLAYED}", fontSize = 10.sp, color = Color.DarkGray, softWrap = false, diff --git a/docs/engineering/android-app/configuring/config-types/widget.mdx b/docs/engineering/android-app/configuring/config-types/widget.mdx index 1230b1e748..44a4c8d5dd 100644 --- a/docs/engineering/android-app/configuring/config-types/widget.mdx +++ b/docs/engineering/android-app/configuring/config-types/widget.mdx @@ -223,6 +223,38 @@ serviceButton | A ButtonProperties object that represents a button displayed in services | A list of ButtonProperties that represent the services associated with the service card | yes | null | actions | A list of ActionConfig objects that represent the actions that can be performed on the service card | yes | emptyList() | + +**serviceMemberIcons**

+One of the config properties of the SERVICE_CARD viewType are the serviceMemberIcons. Some icons provided within fhircore +for use include the ![BABY_BOY](/img/ic_baby_boy.svg), ![BABY_GIRL](/img/ic_baby_girl.svg), ![WOMAN_OF_REPRODUCTIVE_AGE](/img/ic_woman_of_reproductive_age.svg), +![POST_PARTUM_MOTHER](/img/ic_post_partum_mother.svg), ![ELDERLY](/img/ic_elderly.svg), and ![SICK_CHILD](/img/ic_sick_child.svg). + +Since you could have many icons intended for use, it could be better to extract the icons and add a reference to them +within the service card. Below is an example of a list of extraction rules for BABY_BOY and BABY_GIRL. +``` json +{ + "name": "serviceMemberIcons", + "condition": "true", + "actions": [ + "data.put('serviceMemberIcons', StringUtils:join([fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate >= today() - 5 'years') and (Patient.gender= 'male')\") == 'true'? 'BABY_BOY': '', fhirPath.extractValue(Patient, \"Patient.active and (Patient.birthDate >= today() - 5 'years') and (Patient.gender= 'female')\") == 'true'? 'BABY_GIRL': ''], ','))" + ] +} +``` + +After storing the extracted icons into the data map, we can then reference them while configuring the SERVICE_CARD as exemplified +below. + +``` json +{ + "viewType": "SERVICE_CARD", + "details": [ + ... + ], + "serviceMemberIcons": "@{serviceMemberIcons}" +} +``` + + ## COLUMN widgets The column widgets are used to create a vertical layout container that can hold multiple child views. The purpose of the "COLUMN" is to arrange child views in a vertical column, with the child views stacked on top of one another in the order in which they are added. diff --git a/docs/engineering/android-app/developer-setup/code-standards.mdx b/docs/engineering/android-app/developer-setup/code-standards.mdx index 9dda69c8e0..d030bc2cb2 100644 --- a/docs/engineering/android-app/developer-setup/code-standards.mdx +++ b/docs/engineering/android-app/developer-setup/code-standards.mdx @@ -32,9 +32,11 @@ Here are some guidelines when writing a commit message: 6. Wrap the body at 72 characters 7. Use the body to explain what and why vs. how +To dive deeper into these guidelines, please view [this article by Chris Beams](https://cbea.ms/git-commit/). + **Sample commit message:** ``` -Implement Login functionality +Implement login functionality - Add login page view - Implement authentication and credentials management diff --git a/docs/engineering/android-app/developer-setup/keycloak-auth-token-config.md b/docs/engineering/android-app/developer-setup/keycloak-auth-token-config.md deleted file mode 100644 index 00d1858a04..0000000000 --- a/docs/engineering/android-app/developer-setup/keycloak-auth-token-config.md +++ /dev/null @@ -1,39 +0,0 @@ -# Setting up Keycloak - -## Keycloak auth token configuration - -When making API requests, the app uses an access token that represent authorization to access resources on the server. - -When the access token expires, the app will attempt to renew it using a refresh token. - -The access token lifespan is configured on Keycloak as the `Access Token Lifespan`. - -The refresh token lifespan will be equal to the smallest value among (`SSO Session Idle`, `Client Session Idle`, `SSO Session Max`, and `Client Session Max`). - -When setting up identity and access management via Keycloak, the access and refresh token values are required to ensure the access token renewal works as expected in the app. - -## Configuring Keycloak with fhir-web - -- Navigate to the Keycloak Admin UI, e.g `http://keycloak:8080` -- Create realm with name `fhir` or anything else -- In the realm previously created, create new Client Scope "fhir_core_app_id", set it as Default, and add mapper user attribute `fhir_core_app_id` -- Create client for OpenSRP v2.0 client-side application, e.g. `opensrp-v2-app-client` - - In "Capability config", turn on "Client authentication". - - When it is ON, the OIDC type is set to confidential access type. When it is OFF, it is set to public access type) - - Set "Valid redirect URIs" to `*` or other as needed - - Copy & paste the client secret from "Credentials" tab -- Create client for `fhir-web` - - In "Capability config", turn on "Client authentication". - - Set "Valid redirect URIs" to the domain name of your `fhir-web` installation + wildcard as suffix, e.g. `https://fhir-web.example.org/*` or other as needed - - Set "Valid post logout redirect URIs" to the domain name of your `fhir-web` installation + wildcard as suffix, e.g. `https://fhir-web.example.org/*` or other as needed - - Set "Web origins" to the domain name of your `fhir-web` installation, e.g. `https://fhir-web.example.org` or other as needed - - Copy the client secret from the "Credentials" tab, paste it wherever needed -- Create realm roles based on [OpenSRP V2 RBAC ROLES](https://docs.google.com/document/d/1MEw41Rtfdmos9gqqDamQ31_Y58E8Thgo_8i9UXD8ET4/edit) - - **TODO:** Add script + payload to load all these to the Keycloak instance -- Create the groups "Super Admin" and "Provider" - - Add roles in the "Role Mapping" - - Add attribute `fhir_core_app_id` in the group, or in each user - -### Extra notes - -- Create users via `fhir-web`. This helps by automatically creating the additional required FHIR resources of "[Practitioner](http://hl7.org/fhir/R4/practitioner.html)" and "[PractitionerRole](http://hl7.org/fhir/R4/practitionerrole.html)" for new users / healthcare workers. diff --git a/docs/engineering/android-app/developer-setup/publishing-fhir-sdk-artifacts.mdx b/docs/engineering/android-app/developer-setup/publishing-fhir-sdk-artifacts.mdx index 46360b3d4b..d2eec4d3f5 100644 --- a/docs/engineering/android-app/developer-setup/publishing-fhir-sdk-artifacts.mdx +++ b/docs/engineering/android-app/developer-setup/publishing-fhir-sdk-artifacts.mdx @@ -1,3 +1,7 @@ +--- +sidebar_label: Publishing SDK Artifacts +--- + # Publishing FHIR SDK Library Artifacts ### Local Publishing @@ -97,4 +101,4 @@ See related [sample commit here](https://github.com/google/android-fhir/commit/1 Once all the above is in place you just need to run the command:
`./gradlew :datacapture:publishReleasePublicationToSonatypeRepository --stacktrace`. All the other modules follow a similar format, you only need to change the module you are targeting, e.g. to publish _engine_ use the command:
`./gradlew :engine:publishReleasePublicationToSonatypeRepository --stacktrace`

-Your artifact should now be available under the corresponding artifact group under your org. on Sonatype
`https://oss.sonatype.org/content/repositories/snapshots/org/smartregister/data-capture/` \ No newline at end of file +Your artifact should now be available under the corresponding artifact group under your org. on Sonatype
`https://oss.sonatype.org/content/repositories/snapshots/org/smartregister/data-capture/` diff --git a/docs/engineering/android-app/developer-setup/readme.mdx b/docs/engineering/android-app/developer-setup/readme.mdx index 1796269ef5..ef7f5419af 100644 --- a/docs/engineering/android-app/developer-setup/readme.mdx +++ b/docs/engineering/android-app/developer-setup/readme.mdx @@ -45,6 +45,9 @@ FHIR_BASE_URL=https://fhir.labs.smartregister.org/fhir/ #sentry dsn SENTRY_DSN= +#Optional: Application id for a specific build variant +OPENSRP_APP_ID="demo" + ``` **Note:** It is required to configure your OAuth client as **public** hence there's no need for a _OAuth client secret_. diff --git a/docs/engineering/android-app/introduction/readme.mdx b/docs/engineering/android-app/readme.mdx similarity index 100% rename from docs/engineering/android-app/introduction/readme.mdx rename to docs/engineering/android-app/readme.mdx diff --git a/docs/engineering/platform-components/index.md b/docs/engineering/backend/architecture.mdx similarity index 97% rename from docs/engineering/platform-components/index.md rename to docs/engineering/backend/architecture.mdx index d3cb69e935..e57b248388 100644 --- a/docs/engineering/platform-components/index.md +++ b/docs/engineering/backend/architecture.mdx @@ -1,9 +1,4 @@ ---- -sidebar_position: 3 ---- - - -# Platform Components +# Architecture ### FHIR Data Store diff --git a/docs/engineering/backend/info-gateway.mdx b/docs/engineering/backend/info-gateway.mdx new file mode 100644 index 0000000000..677597b1b7 --- /dev/null +++ b/docs/engineering/backend/info-gateway.mdx @@ -0,0 +1,32 @@ +--- +sidebar_label: FHIR Gateway +--- + +# FHIR Information Gateway + +### How it works + +The FHIR Information Gateway is a proxy that sits between the clients and the FHIR API. This allows us to consistently handle authorization agnostic to the system that happens to be providing the FHIR API we are fetching data from, i.e. the client will connect to the FHIR Information Gateway the same way regardless of whether the underlying FHIR API is being provided HAPI FHIR, Google Cloud Healthcare API, Azure Health Data Service, or anything else. + +When using HAPI as the FHIR API, after the FHIR Information Gateway is deployed, the HAPI FHIR backend is deployed with the integrated Keycloak configuration disabled. Any requests made to the backend by the client are now made to the FHIR Information Gateway, which then proxies the request to the HAPI FHIR API and only allows access to the API endpoints if the token provided by the client has the relevant authorization. +> **Note**: In a production environment the FHIR API and data store, e.g. HAPI FHIR backend, would be inaccessible to the public and only accessible from the IP of the FHIR Information Gateway or via a VPN. + +We have written a set of plugins that extend the FHIR Information Gateway functionality to provide features useful to OpenSRP. This includes the following plugins: + + - **Permissions Checker** - Authorization per FHIR Endpoint per HTTP Verb + + - **Data Access Checker** - Data filtering based on user assignment, i.e. filtering by Organization, Location, Practitioner, or CareTeam + + - **Data Requesting** - Data fetching mechanism for FHIR Resources defining patient data vs OpenSRP 2.0 application sync config resources + +### Filtering FHIR API data based on meta tags + +The OpenSRP 2.0 client application has logic that tags all the resources created with meta tags that correspond to the supported sync strategies i.e. Organization, Location, Practitioner, and CareTeam. This way, if we need to change a sync strategy for a deployment or support different strategies for various roles we can change their sync strategy and the relevant data would be downloaded since it is already tagged. + +### How to set up the FHIR Gateway host + +The Gateway setup and configuration is documented here: +- [FHIR Gateway Setup and configuration](https://github.com/google/fhir-gateway) + +- [FHIR Gateway Docker image](https://hub.docker.com/r/opensrp/fhir-gateway/tags) + diff --git a/docs/engineering/backend/keycloak.mdx b/docs/engineering/backend/keycloak.mdx new file mode 100644 index 0000000000..298f8145f4 --- /dev/null +++ b/docs/engineering/backend/keycloak.mdx @@ -0,0 +1,60 @@ +--- +sidebar_label: Keycloak +--- + +# + +## Keycloak user management + +1. Create the user on Keycloak +1. Create the required groups, e.g. create the `PROVIDER` and `SUPERVISOR` groups +1. Create roles for all the resources your application uses, e.g. for permissions on the `Patient` resource create the roles `GET_PATIENT`, `PUT_PATIENT`, `POST_PATIENT`. The KeyCloak definition is as follows: + 1. `HTTP` methods define the permissions a user can have on any endpoint. We also use an additional `Manage` role which is a composite of the 4 `HTTP` method roles + 1. The Permissions checker plugin currently handles the `POST`, `GET`, `PUT`, `DELETE` HTTP methods + 1. The permissions use the following format: `[HTTP_METHOD]_[RESOURCE_NAME]`. Where `RESOURCE_NAME` is the FHIR resource name, e.g `Patient`. + +> **Note:** Keycloak Roles are case sensitive. OpenSRP 2 uses uppercase letters in its role naming. + +4. Assign the roles to the corresponding group, e.g. for the above assign to `PROVIDER` +1. Assign the created Group, e.g. Provider to the user +1. Add a new user attribute with the key `fhir_core_app_id` and a value corresponding to the user’s assigned android client application id on the Composition resource (`composition_config.json`). +1. Create a protocol mapper with Mapper Type `User Attribute` at the client level, area path (Keycloak v20+) `Clients` > `Client Details` > `Dedicated Scopes` > `Add mapper`. The **User attribute** and **Token claim name** field values should match the attribute key `fhir_core_app_id` created in the previous step. + - For keycloak below v20, `Clients` > `your-client-id` >` Mappers` > `Create` + +### Keycloak auth token configuration + +When making API requests, the app uses an access token that represent authorization to access resources on the server. + +When the access token expires, the app will attempt to renew it using a refresh token. + +The access token lifespan is configured on Keycloak as the `Access Token Lifespan`. + +The refresh token lifespan will be equal to the smallest value among (`SSO Session Idle`, `Client Session Idle`, `SSO Session Max`, and `Client Session Max`). + +When setting up identity and access management via Keycloak, the access and refresh token values are required to ensure the access token renewal works as expected in the app. + +## Configuring Keycloak for fhir-web + +1. Navigate to the Keycloak Admin UI, e.g `http://keycloak:8080` +1. Create realm with name `fhir` or anything else +1. In the realm previously created, create new Client Scope "fhir_core_app_id", set it as Default, and add mapper user attribute `fhir_core_app_id` +1. Create client for OpenSRP v2.0 client-side application, e.g. `opensrp-v2-app-client` + - In "Capability config", turn on "Client authentication". + - When it is ON, the OIDC type is set to confidential access type. When it is OFF, it is set to public access type) + - Set "Valid redirect URIs" to `*` or other as needed + - Copy & paste the client secret from "Credentials" tab +1. Create client for `fhir-web` + - In "Capability config", turn on "Client authentication". + - Set "Valid redirect URIs" to the domain name of your `fhir-web` installation + wildcard as suffix, e.g. `https://fhir-web.example.org/*` or other as needed + - Set "Valid post logout redirect URIs" to the domain name of your `fhir-web` installation + wildcard as suffix, e.g. `https://fhir-web.example.org/*` or other as needed + - Set "Web origins" to the domain name of your `fhir-web` installation, e.g. `https://fhir-web.example.org` or other as needed + - Copy the client secret from the "Credentials" tab, paste it wherever needed +1. Create realm roles based on [OpenSRP V2 RBAC ROLES](https://docs.google.com/document/d/1MEw41Rtfdmos9gqqDamQ31_Y58E8Thgo_8i9UXD8ET4/edit) + - **TODO:** Add script + payload to load all these to the Keycloak instance +1. Create the groups "Super Admin" and "Provider" + - Add roles in the "Role Mapping" + - Add attribute `fhir_core_app_id` in the group, or in each user + +### Notes + +- Create users via `fhir-web`. This helps by automatically creating the additional required FHIR resources of "[Practitioner](http://hl7.org/fhir/R4/practitioner.html)" and "[PractitionerRole](http://hl7.org/fhir/R4/practitionerrole.html)" for new users / healthcare workers. diff --git a/docs/engineering/backend/readme.mdx b/docs/engineering/backend/readme.mdx new file mode 100644 index 0000000000..db45cff057 --- /dev/null +++ b/docs/engineering/backend/readme.mdx @@ -0,0 +1,138 @@ +--- +sidebar_label: Backend +--- + +# + +## Backend application setup + +The backend requires at a minimum two pieces of software, with an optional third: + +1. a FHIR Store, e.g HAPI FHIR +2. the [FHIR Information Gateway](https://github.com/onaio/fhir-gateway-plugin) with [OpenSRP plugins](https://hub.docker.com/r/onaio/fhir-gateway-plugin/tags) +3. [Optional] the [fhir-web](https://github.com/onaio/fhir-web) admin dashboard. + +## User management + +You can manage users manually via the APIs and/or user interfaces for keycloak and your FHIR API, or via the fhir-web user interface. See [Keycloak](backend/keycloak) for details. + +### FHIR API user management + +- For each `Practitioner` resource, create a corresponding `Group` resource with the `Practitioner.id` referenced in the `Group.member` attribute. + + 1. This `Group` resource links the `Practitioner` to a `CarePlan` resource when the `Practitioner` is the `CarePlan.subject`. + +- When creating a `Practitioner` resource, create a `PractitionerRole` resource. + + 1. This resource links the `Practitioner` to an `Organization` resource when the `Practitioner` is an `Organization` member. + 1. The `PractitionerRole` resource defines the role of the `Practitioner` in the `Organization`, e.g. a Community Health Worker or Supervisor role. + +- Assign the `Practitioner` a `CareTeam` by adding a `Practitioner` reference to the `CareTeam.participant.member` attribute. + + 1. Assign the `CareTeam` an `Organization` by adding an `Organization` reference to the `CareTeam.managingOrganization` attribute. + 1. Add an `Organization` reference to the `CareTeam.participant.member` attribute of the `CareTeam` resource for easy search. + +- Assign the `Organization` a `Location` via the `OrganizationAffiliation` resource. + + 1. The `Organization` is referenced on the `OrganizationAffiliation.organization` attribute. + 1. The `Location` is referenced on the `OrganizationAffiliation.location` attribute. + +- The `Location` child parent relationship is defined by the `Location.partOf` attribute. + 1. The parent `Location` is referenced on the child's `Location.partOf` attribute. + +## Android application + +- Update `local.properties` file + - Update `FHIR_BASE_URL` value to the `url` of the FHIR Gateway Host + +- Data Filtering - configure sync strategy + - Update the `application_configuration.json` with the sync strategy for the deployment, e.g. for sync by Location: + + ```json + "syncStrategy": ["Location"] + ``` + +> **Note:** Currently the configuration accepts an array but a subsequent update will enforce a single value. See [application_config.json](https://github.com/opensrp/fhircore/blob/main/android/quest/src/main/assets/configs/app/application_config.json) + +- Composition JSON + - Update the identifier to the value of the application id + + ```json + "identifier": { + "use": "official", + "value": "" + } + ``` + +> **Note:** `identifier.value` above should correspond to `fhir_core_app_id` mentioned in the user management Keycloak section below. + +- Update the `sync_config.json` to remove all the non-patient data resources. These should be referenced from the Composition resource so they can be exempted from the Data filter. See [sync_config.json](https://github.com/opensrp/fhircore/blob/b7c24616d4224bd8d16c53b0c2a4f14a1075ce7c/android/quest/src/main/assets/configs/app/sync_config.json) + +## FHIR API and configuration resources + +1. Deploy the FHIR Store, e.g HAPI + + - The steps here depend on what FHIR Store your are using. To deploy the HAPI FHIR Server using JPA, follow [these](https://github.com/hapifhir/hapi-fhir-jpaserver-starter) steps. + +2. `POST` the binary resources referenced in the `composition_config.json` + +> **Note:** As described in the [FHIR Gateway](backend/info-gateway) section, the server should be in an internal network behind a DMZ and therefore not require authentication, which will be handled by the FHIR Information Gateway. + +## Deploy the FHIR Gateway + +1. Link to the [Docker image](https://hub.docker.com/r/onaio/fhir-gateway-plugin/tags ) +1. The main documentation for deploying can be found in the [Github READ ME](https://github.com/google/fhir-gateway/blob/main/README.md)For configuration parameters, check out Read Me file for setting environment variables. +1. For configuration parameters, check out Read Me file for setting environment variables. +1. OpenSRP nuances: Provide/export the System variable `ALLOWED_QUERIES_FILE` with value `"resources/hapi_page_url_allowed_queries.json"`[HAPI Page URL Allowed Queries](https://github.com/opensrp/fhir-gateway/blob/main/resources/hapi_page_url_allowed_queries.json) +1. For each deployment the configuration entries for resources here should match the specific `Composition` resource ID and `Binary` resources IDs +1. Refer to the [FHIR Info Gateway Plugin Deployment Documentation](https://docs.google.com/document/d/1dVFwI3B6AR-J3HTgLdbM-AJuoxMorrOqe2z7y_t_B2Y/edit?usp=sharing) + +## Deploy fhir-web + + 1. The OpenSRP 2.0 web portal deployment docs can be found [here](https://github.com/opensrp/web/blob/master/docs/fhir-web-docker-deployment.md) + 1. This platform doesn’t yet target the Gateway server. We are working to build plugins for it to use. + + +## Gotchas + +- Keycloak redirect - You need to disable [keycloak authentication](https://github.com/opensrp/hapi-fhir-keycloak) in HAPI FHIR + +- Binary resource base64 encoding - You need to make sure that you properly set the Binary resource for application configuration + +- Keycloak/Role configuration - Roles for all the different resources - including `PUT`, `POST`, `GET` for Binary should exist, Client Mapper for the `fhir_core_app_id` and corresponding user attribute should not be missing + +- The `TOKEN_ISSUER` specified in your backend deployment should be the same Realm used by the application to fetch an access token for authentication and authorization. + +``` +env: + - name: TOKEN_ISSUER + value: https://.smartregister.org/auth/realms/FHIR_Android +``` + +- Remove Resource entries from the `sync_confguration.json` file that should not be part of the normal data sync but rather part of the Composition file e.g. Questionnaire + +- When testing the set up **DO NOT** use debug app ids e.g. `app/debug`. The Gateway’s implementation is tightly coupled with the server hosted application resources + +- In the HAPI FHIR application.yaml disable validations by setting to `false*`. This is however not highly recommended. + + + +## Resources + +- [FHIR Gateway](https://github.com/opensrp/fhir-gateway) +- [Permission Checker Spec](https://github.com/opensrp/fhircore/discussions/1603) +- [Data Access Filter Spec](https://github.com/opensrp/fhircore/discussions/1604) +- [Data Requesting Spec](https://github.com/opensrp/fhircore/discussions/1612) +- [FHIR Gateway Tags](https://hub.docker.com/r/opensrp/fhir-gateway/tags) +- [FHIR Web Docker Deployment](https://github.com/opensrp/web/blob/master/docs/fhir-web-docker-deployment.md) +- [OpenSRP Web Issue 1094](https://github.com/opensrp/web/issues/1094) +- [OpenSRP Web Issue 1095](https://github.com/opensrp/web/issues/1095) +- [OpenSRP Web Issue 553](https://github.com/opensrp/web/issues/553) +- [OpenSRP Web Issue 842](https://github.com/opensrp/web/issues/842) +- [OpenSRP Web Issue 552](https://github.com/opensrp/web/issues/552) +- [OpenSRP Web Issue 665](https://github.com/opensrp/web/issues/665) +- [OpenSRP Web Issue 1080](https://github.com/opensrp/web/issues/1080) +- [OpenSRP Web Issue 663](https://github.com/opensrp/web/issues/663) +- [OpenSRP Web Issue 1079](https://github.com/opensrp/web/issues/1079) +- [OpenSRP V2 RBAC ROLES](https://docs.google.com/document/d/1MEw41Rtfdmos9gqqDamQ31_Y58E8Thgo_8i9UXD8ET4) +- [How to Migrate to the Gateway server for sync](https://docs.google.com/document/d/1OeznAQsZe4p2NDiHhpfNKWB2y-qVhgEva5k_GeHTiKc/edit?usp=sharing) diff --git a/static/img/ic_baby_boy.svg b/static/img/ic_baby_boy.svg new file mode 100644 index 0000000000..bff7209822 --- /dev/null +++ b/static/img/ic_baby_boy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/img/ic_baby_girl.svg b/static/img/ic_baby_girl.svg new file mode 100644 index 0000000000..dfab4c82a5 --- /dev/null +++ b/static/img/ic_baby_girl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/img/ic_elderly.svg b/static/img/ic_elderly.svg new file mode 100644 index 0000000000..6219549f9e --- /dev/null +++ b/static/img/ic_elderly.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/img/ic_post_partum_mother.svg b/static/img/ic_post_partum_mother.svg new file mode 100644 index 0000000000..4c03192e3e --- /dev/null +++ b/static/img/ic_post_partum_mother.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/img/ic_sick_child.svg b/static/img/ic_sick_child.svg new file mode 100644 index 0000000000..400fd9e5e1 --- /dev/null +++ b/static/img/ic_sick_child.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/img/ic_woman_of_reproductive_age.svg b/static/img/ic_woman_of_reproductive_age.svg new file mode 100644 index 0000000000..394f3e8941 --- /dev/null +++ b/static/img/ic_woman_of_reproductive_age.svg @@ -0,0 +1,3 @@ + + +