diff --git a/emis/build.gradle.kts b/emis/build.gradle.kts index 854c4ab051..33767d9fad 100644 --- a/emis/build.gradle.kts +++ b/emis/build.gradle.kts @@ -110,7 +110,6 @@ dependencies { implementation(libs.androidx.material3) implementation(libs.kotlinx.coroutines.android) implementation(libs.dagger.hilt.android) - implementation(libs.kotlin.serialization.json) kapt(libs.dagger.hilt.android.compiler) diff --git a/emis/src/main/java/org/saudigitus/emis/MainActivity.kt b/emis/src/main/java/org/saudigitus/emis/MainActivity.kt index f1e6b68957..d43d9553a0 100644 --- a/emis/src/main/java/org/saudigitus/emis/MainActivity.kt +++ b/emis/src/main/java/org/saudigitus/emis/MainActivity.kt @@ -54,6 +54,8 @@ class MainActivity : FragmentActivity() { startDestination = AppRoutes.HOME_ROUTE, ) { composable(AppRoutes.HOME_ROUTE) { + viewModel.setProgram(intent?.extras?.getString(Constants.PROGRAM_UID) ?: "") + HomeScreen( viewModel = viewModel, onBack = { finish() }, diff --git a/emis/src/main/java/org/saudigitus/emis/data/local/DataManager.kt b/emis/src/main/java/org/saudigitus/emis/data/local/DataManager.kt index 5f994c75c6..a0012f2458 100644 --- a/emis/src/main/java/org/saudigitus/emis/data/local/DataManager.kt +++ b/emis/src/main/java/org/saudigitus/emis/data/local/DataManager.kt @@ -20,7 +20,16 @@ interface DataManager { ) suspend fun getConfig(id: String): List? + /** + * @param ou OrganizationUnit uid + * @param program Program uid + * @param dataElement DataElement uid + * + * Set ou uid and program uid to apply the program rules + */ suspend fun getOptions( + ou: String?, + program: String?, dataElement: String, ): List diff --git a/emis/src/main/java/org/saudigitus/emis/data/local/repository/DataManagerImpl.kt b/emis/src/main/java/org/saudigitus/emis/data/local/repository/DataManagerImpl.kt index 599f0967d9..1528100cd3 100644 --- a/emis/src/main/java/org/saudigitus/emis/data/local/repository/DataManagerImpl.kt +++ b/emis/src/main/java/org/saudigitus/emis/data/local/repository/DataManagerImpl.kt @@ -26,6 +26,7 @@ import org.saudigitus.emis.data.model.ProgramStage import org.saudigitus.emis.data.model.Subject import org.saudigitus.emis.data.model.dto.AttendanceEntity import org.saudigitus.emis.data.model.dto.withBtnSettings +import org.saudigitus.emis.service.RuleEngineRepository import org.saudigitus.emis.ui.attendance.AttendanceOption import org.saudigitus.emis.ui.components.DropdownItem import org.saudigitus.emis.utils.Constants @@ -34,6 +35,7 @@ import org.saudigitus.emis.utils.Utils import org.saudigitus.emis.utils.eventsWithTrackedDataValues import org.saudigitus.emis.utils.optionByOptionSet import org.saudigitus.emis.utils.optionsByOptionSetAndCode +import org.saudigitus.emis.utils.optionsNotInOptionGroup import timber.log.Timber import java.sql.Date import javax.inject.Inject @@ -42,6 +44,7 @@ class DataManagerImpl @Inject constructor( val d2: D2, val networkUtils: NetworkUtils, + val ruleEngineRepository: RuleEngineRepository, ) : DataManager { private fun getAttributeOptionCombo() = d2.categoryModule().categoryOptionCombos() @@ -133,17 +136,36 @@ class DataManagerImpl } override suspend fun getOptions( + ou: String?, + program: String?, dataElement: String, ): List = withContext(Dispatchers.IO) { val optionSet = d2.dataElement(dataElement)?.optionSetUid() - return@withContext d2.optionByOptionSet(optionSet).map { - DropdownItem( - id = it.uid(), - itemName = "${it.displayName()}", - code = it.code() ?: "", - sortOrder = it.sortOrder(), - ) + val hideOptions = if (ou != null && program != null) { + ruleEngineRepository.applyOptionRules(ou, program, dataElement) + } else { + emptyList() + } + + return@withContext if (hideOptions.isEmpty()) { + d2.optionByOptionSet(optionSet).map { + DropdownItem( + id = it.uid(), + itemName = "${it.displayName()}", + code = it.code() ?: "", + sortOrder = it.sortOrder(), + ) + } + } else { + d2.optionsNotInOptionGroup(hideOptions, optionSet).map { + DropdownItem( + id = it.uid(), + itemName = "${it.displayName()}", + code = it.code() ?: "", + sortOrder = it.sortOrder(), + ) + }.sortedBy { it.sortOrder } } } diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/Favorite.kt b/emis/src/main/java/org/saudigitus/emis/data/model/Favorite.kt deleted file mode 100644 index 4f96426be1..0000000000 --- a/emis/src/main/java/org/saudigitus/emis/data/model/Favorite.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.saudigitus.emis.data.model - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.Serializable - -@JsonIgnoreProperties(ignoreUnknown = true) -@Serializable -data class Favorite( - val uid: String = "", - @JsonProperty("school") - val school: String? = null, - @JsonProperty("stream") - val stream: List = emptyList(), -) diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/FavoriteConfig.kt b/emis/src/main/java/org/saudigitus/emis/data/model/FavoriteConfig.kt deleted file mode 100644 index f4cc71137f..0000000000 --- a/emis/src/main/java/org/saudigitus/emis/data/model/FavoriteConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.saudigitus.emis.data.model - -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.Serializable - -@Serializable -data class FavoriteConfig( - @JsonProperty("favorites") - val favorites: List? = emptyList(), -) diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/Section.kt b/emis/src/main/java/org/saudigitus/emis/data/model/Section.kt deleted file mode 100644 index 50422fc493..0000000000 --- a/emis/src/main/java/org/saudigitus/emis/data/model/Section.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.saudigitus.emis.data.model - -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.Serializable - -@Serializable -data class Section( - @JsonProperty("code") - val code: String?, - @JsonProperty("displayName") - val displayName: String?, -) diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/Stream.kt b/emis/src/main/java/org/saudigitus/emis/data/model/Stream.kt deleted file mode 100644 index 0d53404e55..0000000000 --- a/emis/src/main/java/org/saudigitus/emis/data/model/Stream.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.saudigitus.emis.data.model - -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.Serializable - -@Serializable -data class Stream( - @JsonProperty("grade") - var grade: String? = null, - @JsonProperty("code") - val code: String? = null, - @JsonProperty("sections") - val sections: List
= emptyList(), -) diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/mapper/Favorite.kt b/emis/src/main/java/org/saudigitus/emis/data/model/mapper/Favorite.kt deleted file mode 100644 index 2b9676854f..0000000000 --- a/emis/src/main/java/org/saudigitus/emis/data/model/mapper/Favorite.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.saudigitus.emis.data.model.mapper - -import org.saudigitus.emis.data.model.Favorite -import org.saudigitus.emis.data.model.Section -import org.saudigitus.emis.data.model.Stream - -fun Favorite.mapStream( - streams: List, -) = Favorite( - uid = this.uid, - school = this.school, - stream = streams, -) - -fun Stream.mapSections( - sections: List
, -) = Stream( - grade = this.grade, - sections = sections, -) diff --git a/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt b/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt index a556c91eaf..ac6eddc0bf 100644 --- a/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt +++ b/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt @@ -14,6 +14,7 @@ import org.saudigitus.emis.data.local.DataManager import org.saudigitus.emis.data.local.FormRepository import org.saudigitus.emis.data.local.repository.DataManagerImpl import org.saudigitus.emis.data.local.repository.FormRepositoryImpl +import org.saudigitus.emis.service.RuleEngineRepository import javax.inject.Singleton @Module @@ -26,12 +27,17 @@ object AppModule { @ApplicationContext context: Context, ): NetworkUtils = NetworkUtils(context) + @Provides + @Singleton + fun providesRuleEngineRepository(d2: D2) = RuleEngineRepository(d2) + @Provides @Singleton fun providesDataManager( d2: D2, networkUtils: NetworkUtils, - ): DataManager = DataManagerImpl(d2, networkUtils) + ruleEngineRepository: RuleEngineRepository, + ): DataManager = DataManagerImpl(d2, networkUtils, ruleEngineRepository) @Provides @Singleton diff --git a/emis/src/main/java/org/saudigitus/emis/service/RuleEngineRepository.kt b/emis/src/main/java/org/saudigitus/emis/service/RuleEngineRepository.kt new file mode 100644 index 0000000000..2fd684ea09 --- /dev/null +++ b/emis/src/main/java/org/saudigitus/emis/service/RuleEngineRepository.kt @@ -0,0 +1,231 @@ +package org.saudigitus.emis.service + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.dhis2.commons.bindings.event +import org.dhis2.commons.bindings.organisationUnit +import org.dhis2.commons.bindings.programStage +import org.dhis2.commons.rules.RuleEngineContextData +import org.dhis2.commons.rules.toRuleEngineInstant +import org.dhis2.commons.rules.toRuleEngineLocalDate +import org.dhis2.form.bindings.toRuleDataValue +import org.dhis2.form.bindings.toRuleEngineObject +import org.dhis2.form.bindings.toRuleVariable +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.program.ProgramRuleActionType +import org.hisp.dhis.rules.api.RuleEngine +import org.hisp.dhis.rules.api.RuleEngineContext +import org.hisp.dhis.rules.models.Rule +import org.hisp.dhis.rules.models.RuleEvent +import org.hisp.dhis.rules.models.RuleVariable +import javax.inject.Inject + +class RuleEngineRepository @Inject constructor( + private val d2: D2, +) { + + private val ruleEngine by lazy { RuleEngine.getInstance() } + + private suspend fun supplementaryData(ou: String) = withContext(Dispatchers.IO) { + val suppData = HashMap>() + + d2.organisationUnitModule().organisationUnits() + .withOrganisationUnitGroups() + .uid(ou).blockingGet() + .let { orgUnit -> + orgUnit?.organisationUnitGroups()?.mapNotNull { + if (it.code() != null) { + suppData[it.code()!!] = listOf(orgUnit.uid()) + } + suppData[it.uid()] = listOf(orgUnit.uid()) + } + } + + return@withContext suppData + } + + private suspend fun ruleVariables(program: String) = withContext(Dispatchers.IO) { + return@withContext d2.programModule().programRuleVariables() + .byProgramUid().eq(program) + .blockingGet() + .map { + it.toRuleVariable( + d2.trackedEntityModule().trackedEntityAttributes(), + d2.dataElementModule().dataElements(), + d2.optionModule().options(), + ) + } + } + + suspend fun rules(program: String) = withContext(Dispatchers.IO) { + return@withContext d2.programModule().programRules() + .byProgramUid().eq(program) + .withProgramRuleActions() + .blockingGet() + .map { + it.toRuleEngineObject() + } + } + + suspend fun constants() = withContext(Dispatchers.IO) { + return@withContext d2.constantModule() + .constants().blockingGet() + .associate { constant -> + Pair(constant.uid(), "${constant.value()}") + } + } + + @Suppress("DEPRECATION") + private suspend fun ruleEvents( + ou: String, + program: String, + ) = withContext(Dispatchers.IO) { + return@withContext d2.eventModule().events() + .byOrganisationUnitUid().eq(ou) + .byProgramUid().eq(program) + .withTrackedEntityDataValues() + .blockingGet() + .map { event -> + RuleEvent( + event = event.uid(), + programStage = event.programStage()!!, + programStageName = d2.programModule().programStages() + .uid(event.programStage()) + .blockingGet()!!.name()!!, + status = if (event.status() == EventStatus.VISITED) { + RuleEvent.Status.ACTIVE + } else { + RuleEvent.Status.valueOf(event.status()!!.name) + }, + eventDate = Instant.fromEpochMilliseconds(event.eventDate()!!.time), + dueDate = event.dueDate()?.let { + Instant.fromEpochMilliseconds(it.time) + .toLocalDateTime(TimeZone.currentSystemDefault()).date + }, + completedDate = event.completedDate()?.let { + Instant.fromEpochMilliseconds(it.time) + .toLocalDateTime(TimeZone.currentSystemDefault()).date + }, + organisationUnit = event.organisationUnit()!!, + organisationUnitCode = d2.organisationUnitModule().organisationUnits() + .uid( + event.organisationUnit(), + ).blockingGet()?.code(), + dataValues = event.trackedEntityDataValues()?.toRuleDataValue( + event, + d2.dataElementModule().dataElements(), + d2.programModule().programRuleVariables(), + d2.optionModule().options(), + ) ?: emptyList(), + ) + } + } + + private suspend fun ruleContext( + ruleVariables: List, + rules: List, + supplementaryData: Map>, + constants: Map, + ) = withContext(Dispatchers.IO) { + return@withContext RuleEngineContext( + rules = rules, + ruleVariables = ruleVariables, + supplementaryData = supplementaryData, + constantsValues = constants, + ) + } + + private suspend fun executeContext( + ou: String, + program: String, + ) = withContext(Dispatchers.IO) { + val rules = async { rules(program) }.await() + val ruleVariables = ruleVariables(program) + val constants = async { constants() }.await() + val supplementaryData = async { supplementaryData(ou) }.await() + + return@withContext ruleContext( + ruleVariables, + rules, + supplementaryData, + constants, + ) + } + + private suspend fun ruleEngineContextData( + ou: String, + program: String, + ) = withContext(Dispatchers.IO) { + val rules = async { rules(program) }.await() + val ruleVariables = ruleVariables(program) + val constants = async { constants() }.await() + val ruleEvents = async { ruleEvents(ou, program) }.await() + val supplementaryData = async { supplementaryData(ou) }.await() + + return@withContext RuleEngineContextData( + ruleEngineContext = ruleContext( + ruleVariables, + rules, + supplementaryData, + constants, + ), + ruleEnrollment = null, + ruleEvents = ruleEvents, + ) + } + + private fun getRuleEvent(eventUid: String): RuleEvent { + val event = d2.event(eventUid) ?: throw NullPointerException() + return RuleEvent( + event = event.uid(), + programStage = event.programStage()!!, + programStageName = d2.programStage(event.programStage()!!)?.name()!!, + status = RuleEvent.Status.valueOf(event.status()!!.name), + eventDate = event.eventDate()!!.toRuleEngineInstant(), + dueDate = event.dueDate()?.toRuleEngineLocalDate(), + completedDate = event.completedDate()?.toRuleEngineLocalDate(), + organisationUnit = event.organisationUnit()!!, + organisationUnitCode = d2.organisationUnit(event.organisationUnit()!!)?.code(), + dataValues = emptyList(), + ) + } + + suspend fun applyOptionRules( + ou: String, + program: String, + dataElement: String, + ) = withContext(Dispatchers.IO) { + val ruleContext = async { executeContext(ou, program) }.await() + + val actions = ruleContext.rules.flatMap { it.actions } + .filter { it.type == ProgramRuleActionType.HIDEOPTIONGROUP.name } + + return@withContext actions.mapNotNull { + if (it.values["field"] == dataElement) { + it.values["optionGroup"] + } else { + null + } + } + } + + suspend fun evaluate( + ou: String, + program: String, + event: String, + ) = withContext(Dispatchers.IO) { + val ruleEngineContextData = ruleEngineContextData(ou, program) + + return@withContext ruleEngine.evaluate( + target = getRuleEvent(event), + ruleEnrollment = ruleEngineContextData.ruleEnrollment, + ruleEvents = ruleEngineContextData.ruleEvents, + executionContext = ruleEngineContextData.ruleEngineContext, + ) + } +} diff --git a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceViewModel.kt b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceViewModel.kt index 44bd8dae03..b060c95054 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceViewModel.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceViewModel.kt @@ -191,7 +191,7 @@ class AttendanceViewModel private fun getReasonForAbsence(dataElement: String) { viewModelScope.launch { - _reasonOfAbsence.value = repository.getOptions(dataElement) + _reasonOfAbsence.value = repository.getOptions(null, null, dataElement) } } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeViewModel.kt b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeViewModel.kt index ce725f97af..7ceb2cf488 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeViewModel.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeViewModel.kt @@ -85,7 +85,9 @@ class HomeViewModel } } - override fun setProgram(program: String) {} + override fun setProgram(program: String) { + _program.value = program + } override fun setDate(date: String) {} override fun save() {} @@ -152,20 +154,36 @@ class HomeViewModel } fun setSchool(ou: OU?) { - _filterState.update { - it.copy(school = ou) - } + viewModelScope.launch { + _filterState.update { + it.copy(school = ou) + } + setOU("${ou?.uid}") - val subtitle = if (filterState.value.academicYear?.itemName != null) { - "${filterState.value.academicYear?.itemName} | ${ou?.displayName}" - } else { - ou?.displayName - } + val subtitle = if (filterState.value.academicYear?.itemName != null) { + "${filterState.value.academicYear?.itemName} | ${ou?.displayName}" + } else { + ou?.displayName + } - _toolbarHeader.update { - it.copy(subtitle = subtitle) + _toolbarHeader.update { + it.copy(subtitle = subtitle) + } + + val filters = dataElementFilters.value.toMutableList() + filters.removeAt(1) + filters.add( + index = 1, + DropdownState( + FilterType.GRADE, + getDataElementName("${registration.value?.grade}"), + options("${registration.value?.grade}"), + ), + ) + + _dataElementFilters.value = filters } - setOU("${ou?.uid}") + getTeis() } @@ -183,5 +201,9 @@ class HomeViewModel getTeis() } - private suspend fun options(uid: String) = repository.getOptions(uid) + private suspend fun options(uid: String) = repository.getOptions( + ou = filterState.value.school?.uid, + program = program.value, + dataElement = uid, + ) } diff --git a/emis/src/main/java/org/saudigitus/emis/utils/Extensions.kt b/emis/src/main/java/org/saudigitus/emis/utils/Extensions.kt index 3466a34b9d..2059ac20a4 100644 --- a/emis/src/main/java/org/saudigitus/emis/utils/Extensions.kt +++ b/emis/src/main/java/org/saudigitus/emis/utils/Extensions.kt @@ -26,6 +26,26 @@ fun D2.optionByOptionSet( .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) .blockingGet() +fun D2.optionsNotInOptionGroup( + optionGroups: List, + optionSet: String?, +): List