From 60ea0e7a73a7a416d1abc54c96f323cca06c9d64 Mon Sep 17 00:00:00 2001 From: Carlos Macaneta <56470814+CarlosMacaneta@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:08:56 +0200 Subject: [PATCH 1/4] bug fix (#61) * build: app version * feat: disable mandatory filters for data loading --- .../data/local/repository/FormRepositoryImpl.kt | 7 ------- .../emis/ui/attendance/AttendanceScreen.kt | 2 +- .../org/saudigitus/emis/ui/components/Metadata.kt | 9 ++------- .../org/saudigitus/emis/ui/home/HomeUiState.kt | 15 +++------------ .../org/saudigitus/emis/ui/home/HomeViewModel.kt | 6 +++--- gradle/libs.versions.toml | 2 +- 6 files changed, 10 insertions(+), 31 deletions(-) diff --git a/emis/src/main/java/org/saudigitus/emis/data/local/repository/FormRepositoryImpl.kt b/emis/src/main/java/org/saudigitus/emis/data/local/repository/FormRepositoryImpl.kt index 98359fe378b..19b9ae48dcc 100644 --- a/emis/src/main/java/org/saudigitus/emis/data/local/repository/FormRepositoryImpl.kt +++ b/emis/src/main/java/org/saudigitus/emis/data/local/repository/FormRepositoryImpl.kt @@ -106,13 +106,6 @@ class FormRepositoryImpl stage: String, dl: String, ) = withContext(Dispatchers.IO) { - d2.programModule().programTrackedEntityAttributes() - .byProgram().eq("") - .bySearchable().isTrue - .byDisplayInList().isTrue - .blockingGet() - - d2.programModule().programStageDataElements() .byProgramStage().eq(stage) .byDataElement().eq(dl) diff --git a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt index c3f77337123..b099364507e 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt @@ -294,7 +294,7 @@ fun AttendanceScreen( viewModel.setAttendance( index, ou, - tei , + tei, student.selectedEnrollment.uid().orEmpty(), value, reasonOfAbsence, diff --git a/emis/src/main/java/org/saudigitus/emis/ui/components/Metadata.kt b/emis/src/main/java/org/saudigitus/emis/ui/components/Metadata.kt index 47cbf28cb6a..174cd6ac19e 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/components/Metadata.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/components/Metadata.kt @@ -229,13 +229,8 @@ data class InfoCard( val isStaff: Boolean = false, ) { fun hasData(): Boolean { - return if (isStaff) { - academicYear.isNotEmpty() && - orgUnitName.isNotEmpty() - } else { - grade.isNotEmpty() && section.isNotEmpty() && academicYear.isNotEmpty() && - orgUnitName.isNotEmpty() - } + return academicYear.isNotEmpty() && + orgUnitName.isNotEmpty() } } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt index b6edb6ac89c..8a7fb1301a4 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt @@ -27,26 +27,17 @@ data class HomeUiState( val modules: List = emptyList(), ) { val isNull: Boolean - get() = academicYear == null && school == null && grade == null && section == null - - val isStaffFiltersNotNull: Boolean - get() = key == Constants.STAFF && academicYear != null && school != null + get() = academicYear == null && school == null val isStaff: Boolean get() = key == Constants.STAFF val options: List - get() = if (key == Constants.STAFF && academicYear != null && school != null) { - listOf( + get() = if (academicYear != null && school != null) { + listOfNotNull( academicYear.code, grade?.code, section?.code, - ).mapNotNull { it } - } else if (!isNull) { - listOf( - "${academicYear?.code}", - "${grade?.code}", - "${section?.code}", ) } else { emptyList() 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 4912078604c..245ea4c7ef9 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 @@ -130,12 +130,12 @@ class HomeViewModel private fun getTeis() { viewModelScope.launch { - if (!viewModelState.value.isNull || viewModelState.value.isStaffFiltersNotNull) { - val dataElements = listOf( + if (!viewModelState.value.isNull) { + val dataElements = listOfNotNull( registration.value?.academicYear, registration.value?.grade, registration.value?.section, - ).mapNotNull { it } + ) repository.getTeisBy( ou = "${viewModelState.value.school?.uid}", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9081d183340..ee48431cf77 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ kotlin = '2.0.20' hilt = '2.47' androidxNavigation = "2.6.0" androidxHiltNavigation = "1.0.0" -semis = "1.0.0" +semis = "1.4.1" jacoco = '0.8.10' designSystem = "0.4.0.1" From 84e59086f35b0d4ecd2721d06e2341a06f584ab8 Mon Sep 17 00:00:00 2001 From: Carlos Macaneta <56470814+CarlosMacaneta@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:16:23 +0200 Subject: [PATCH 2/4] feat: implementing calendar date range based on academic year (#62) --- .../emis/ui/attendance/AttendanceScreen.kt | 18 +++++++++++++++--- .../saudigitus/emis/ui/components/TopAppBar.kt | 2 +- .../org/saudigitus/emis/ui/components/Utils.kt | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt index b099364507e..b03cfcd01ec 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt @@ -54,6 +54,8 @@ import org.saudigitus.emis.ui.components.ToolbarActionState import org.saudigitus.emis.ui.teis.mapper.TEICardMapper import org.saudigitus.emis.ui.theme.light_success import org.saudigitus.emis.utils.DateHelper +import org.saudigitus.emis.utils.DateHelper.stringToLocalDate +import java.time.ZoneId @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -150,17 +152,27 @@ fun AttendanceScreen( ), calendarAction = viewModel::setDate, dateValidator = { - val date = DateHelper.stringToLocalDate(DateHelper.formatDate(it)!!) + val date = stringToLocalDate(DateHelper.formatDate(it)!!) val today = System.currentTimeMillis() if (schoolCalendar != null) { - ( + val startDate = schoolCalendar?.academicYear?.startDate + val endDate = schoolCalendar?.academicYear?.endDate + + val startMillis = stringToLocalDate(startDate!!) + .atStartOfDay(ZoneId.systemDefault()) + ?.toInstant()?.toEpochMilli()!! + val endMillis = stringToLocalDate(endDate!!) + .atStartOfDay(ZoneId.systemDefault()) + ?.toInstant()?.toEpochMilli()!! + + ( !DateHelper.isWeekend(date) && schoolCalendar?.weekDays?.saturday == false && schoolCalendar?.weekDays?.sunday == false ) && schoolCalendar?.holidays?.let { holiday -> DateHelper.isHoliday(holiday, it) - } == true && it <= today + } == true && (it in startMillis..endMillis) } else { it <= today } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/components/TopAppBar.kt b/emis/src/main/java/org/saudigitus/emis/ui/components/TopAppBar.kt index 4aa41607da5..84c648e35cc 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/components/TopAppBar.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/components/TopAppBar.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.saudigitus.emis.R +import java.time.LocalDate data class ToolbarHeaders( val title: String, @@ -111,7 +112,6 @@ fun Toolbar( actionState: ToolbarActionState = ToolbarActionState(), calendarAction: (date: String) -> Unit = {}, dateValidator: (Long) -> Boolean = { true }, - favoriteAction: () -> Unit = {}, syncAction: () -> Unit = {}, filterAction: () -> Unit = {}, ) { diff --git a/emis/src/main/java/org/saudigitus/emis/ui/components/Utils.kt b/emis/src/main/java/org/saudigitus/emis/ui/components/Utils.kt index 88f9a0e9841..2d89e602434 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/components/Utils.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/components/Utils.kt @@ -40,6 +40,8 @@ import org.saudigitus.emis.R import org.saudigitus.emis.ui.theme.light_error import org.saudigitus.emis.ui.theme.light_info import org.saudigitus.emis.utils.DateHelper +import java.time.LocalDate +import java.time.ZoneId @Composable fun NoResults( From ac5342797c36c035087928b2c348c58445dfd15e Mon Sep 17 00:00:00 2001 From: Carlos Macaneta <56470814+CarlosMacaneta@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:32:20 +0200 Subject: [PATCH 3/4] feat: Implementing filters based on datastore (#63) --- .../emis/data/model/EMISConfigItem.kt | 2 + .../org/saudigitus/emis/data/model/Filter.kt | 10 +++ .../emis/data/model/FilterDataElement.kt | 16 ++++ .../emis/ui/attendance/AttendanceScreen.kt | 2 +- .../emis/ui/components/DropdownComponet.kt | 1 + .../org/saudigitus/emis/ui/home/HomeScreen.kt | 61 +++++++------ .../saudigitus/emis/ui/home/HomeUiState.kt | 1 + .../saudigitus/emis/ui/home/HomeViewModel.kt | 85 +++++++++++-------- 8 files changed, 111 insertions(+), 67 deletions(-) create mode 100644 emis/src/main/java/org/saudigitus/emis/data/model/Filter.kt create mode 100644 emis/src/main/java/org/saudigitus/emis/data/model/FilterDataElement.kt diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/EMISConfigItem.kt b/emis/src/main/java/org/saudigitus/emis/data/model/EMISConfigItem.kt index ae7f73bfd9f..05b935c0525 100644 --- a/emis/src/main/java/org/saudigitus/emis/data/model/EMISConfigItem.kt +++ b/emis/src/main/java/org/saudigitus/emis/data/model/EMISConfigItem.kt @@ -11,6 +11,8 @@ data class EMISConfigItem( val modules: List = emptyList(), @JsonProperty("defaults") val default: DefaultConfig?, + @JsonProperty("filters") + val filter: Filter?, @JsonProperty("key") val key: String?, @JsonProperty("lastUpdate") diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/Filter.kt b/emis/src/main/java/org/saudigitus/emis/data/model/Filter.kt new file mode 100644 index 00000000000..1b5828a2ff0 --- /dev/null +++ b/emis/src/main/java/org/saudigitus/emis/data/model/Filter.kt @@ -0,0 +1,10 @@ +package org.saudigitus.emis.data.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Filter( + @JsonProperty("dataElements") + val dataElements: List +) diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/FilterDataElement.kt b/emis/src/main/java/org/saudigitus/emis/data/model/FilterDataElement.kt new file mode 100644 index 00000000000..fb6680f562d --- /dev/null +++ b/emis/src/main/java/org/saudigitus/emis/data/model/FilterDataElement.kt @@ -0,0 +1,16 @@ +package org.saudigitus.emis.data.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class FilterDataElement( + @JsonProperty("code") + val code: String?, + @JsonProperty("dataElement") + val dataElement: String?, + @JsonProperty("label") + val label: String?, + @JsonProperty("order") + val order: Int? +) diff --git a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt index b03cfcd01ec..8d371974e76 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceScreen.kt @@ -172,7 +172,7 @@ fun AttendanceScreen( ) && schoolCalendar?.holidays?.let { holiday -> DateHelper.isHoliday(holiday, it) - } == true && (it in startMillis..endMillis) + } == true && (it in startMillis..endMillis) && it <= today } else { it <= today } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/components/DropdownComponet.kt b/emis/src/main/java/org/saudigitus/emis/ui/components/DropdownComponet.kt index 55e1fc888af..540e639af6c 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/components/DropdownComponet.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/components/DropdownComponet.kt @@ -72,6 +72,7 @@ data class DropdownState( val leadingIcon: ImageVector? = null, val trailingIcon: ImageVector? = null, val displayName: String = "", + val order: Int = -1, val data: List = emptyList(), ) diff --git a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeScreen.kt index ca34749f203..88b36431912 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeScreen.kt @@ -157,23 +157,22 @@ fun HomeUI( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), horizontalAlignment = Alignment.Start, ) { - uiState.dataElementFilters.firstOrNull { it.filterType == FilterType.ACADEMIC_YEAR } - ?.let { filter -> - AnimatedVisibility(visible = filter.data.isNotEmpty()) { - DropDownWithSelectionByCode( - dropdownState = filter, - defaultSelection = uiState.filterSelection.first, - onItemClick = { item -> - onEvent( - HomeUiEvent.OnFilterChange( - FilterType.ACADEMIC_YEAR, - item - ) + uiState.academicYearState?.let { + AnimatedVisibility(visible = it.data.isNotEmpty()) { + DropDownWithSelectionByCode( + dropdownState = it, + defaultSelection = uiState.filterSelection.first, + onItemClick = { item -> + onEvent( + HomeUiEvent.OnFilterChange( + FilterType.ACADEMIC_YEAR, + item ) - } - ) - } + ) + } + ) } + } AnimatedVisibility(visible = true) { DropDownOu( @@ -187,25 +186,23 @@ fun HomeUI( } ) } - uiState.dataElementFilters - .filter { it.filterType == FilterType.GRADE || it.filterType == FilterType.SECTION } - .forEach { filter -> - val defaultSelection = when (filter.filterType) { - FilterType.GRADE -> uiState.filterSelection.second - FilterType.SECTION -> uiState.filterSelection.third - else -> null - } + uiState.dataElementFilters.forEach { filter -> + val defaultSelection = when (filter.filterType) { + FilterType.GRADE -> uiState.filterSelection.second + FilterType.SECTION -> uiState.filterSelection.third + else -> null + } - AnimatedVisibility(visible = filter.data.isNotEmpty()) { - DropDown( - dropdownState = filter, - defaultSelection = defaultSelection, - onItemClick = { item -> - onEvent(HomeUiEvent.OnFilterChange(filter.filterType, item)) - } - ) - } + AnimatedVisibility(visible = filter.data.isNotEmpty()) { + DropDown( + dropdownState = filter, + defaultSelection = defaultSelection, + onItemClick = { item -> + onEvent(HomeUiEvent.OnFilterChange(filter.filterType, item)) + } + ) } + } Button( modifier = Modifier .fillMaxWidth() diff --git a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt index 8a7fb1301a4..de6db5dcbda 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/home/HomeUiState.kt @@ -20,6 +20,7 @@ data class HomeUiState( val section: DropdownItem? = null, val key: String? = null, val trackedEntityType: String = "", + val academicYearState: DropdownState? = null, val dataElementFilters: List = emptyList(), val toolbarHeaders: ToolbarHeaders = ToolbarHeaders(""), val programSettings: Bundle? = null, 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 245ea4c7ef9..2949a9c3a71 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 @@ -15,6 +15,7 @@ import org.dhis2.commons.Constants.DATA_SET_NAME import org.dhis2.commons.Constants.PROGRAM_UID import org.saudigitus.emis.data.local.DataManager import org.saudigitus.emis.data.model.DefaultConfig +import org.saudigitus.emis.data.model.Filter import org.saudigitus.emis.data.model.OU import org.saudigitus.emis.data.model.Registration import org.saudigitus.emis.helper.ISEMISSync @@ -28,12 +29,6 @@ import org.saudigitus.emis.utils.Constants import javax.inject.Inject -private data class FilterSpec( - val type: FilterType, - val idProvider: Registration.() -> String? -) - - @HiltViewModel class HomeViewModel @Inject constructor( @@ -41,6 +36,9 @@ class HomeViewModel private val semisSync: ISEMISSync, ) : BaseViewModel(repository) { + private val _filter = MutableStateFlow(null) + private val filter: StateFlow = _filter + private val _registration = MutableStateFlow(null) private val registration: StateFlow = _registration @@ -66,38 +64,54 @@ class HomeViewModel private suspend fun loadFiltersSequentially( defaultConfig: DefaultConfig? = null ) { - val specs = listOf( - FilterSpec(FilterType.ACADEMIC_YEAR) { academicYear }, - FilterSpec(FilterType.GRADE) { grade }, - FilterSpec(FilterType.SECTION) { section } + val filterType = mapOf( + "grade" to FilterType.GRADE, + "class" to FilterType.SECTION ) val results = mutableListOf() - specs.forEach { spec -> - val elementId = registration.value?.run(spec.idProvider).orEmpty() + val academicYearId = registration.value?.academicYear + var academicYearState: DropdownState? = null + + academicYearId?.let { + val options = options(academicYearId) + val displayName = getDataElementName(academicYearId) + + academicYearState = DropdownState( + FilterType.ACADEMIC_YEAR, + displayName = displayName, + data = options, + ) + + setAcademicYear(options.find { it.code == defaultConfig?.currentAcademicYear }) + } + + filter.value?.dataElements?.forEach { item -> + val elementId = item.dataElement.orEmpty() if (elementId.isBlank()) return@forEach val options = options(elementId) val displayName = getDataElementName(elementId) + val type = filterType.getOrDefault(item.code, FilterType.NONE) val state = DropdownState( - spec.type, - null, - null, - displayName, - options, + type, + displayName = displayName, + order = item.order ?: 0, + data = options, ) results.add(state) } + + results.sortBy { r -> r.order } viewModelState.update { - it.copy(dataElementFilters = results) + it.copy( + academicYearState = academicYearState, + dataElementFilters = results + ) } - setAcademicYear( - results.find { it.filterType == FilterType.ACADEMIC_YEAR } - ?.data?.find { it.code == defaultConfig?.currentAcademicYear }, - ) } @@ -110,6 +124,8 @@ class HomeViewModel if (config != null) { val defaultConfig = config.default _registration.value = config.registration + _filter.value = config.filter + viewModelState.update { it.copy( key = config.key, @@ -227,19 +243,20 @@ class HomeViewModel private suspend fun reloadFilters(): MutableList { val filters = uiState.value.dataElementFilters.toMutableList() - if (filters.size > 1) { - filters.removeAt(1) - filters.add( - index = 1, - DropdownState( - FilterType.GRADE, - null, - null, - getDataElementName("${registration.value?.grade}"), - options("${registration.value?.grade}"), - ), - ) + if (filters.isNotEmpty()) { + val isRemoved = filters.removeIf { f -> f.filterType == FilterType.GRADE } + + if (isRemoved) { + filters.add( + DropdownState( + FilterType.GRADE, + displayName = getDataElementName("${registration.value?.grade}"), + data = options("${registration.value?.grade}"), + ), + ) + } } + filters.sortBy { it.order } return filters } From ed4b7ed80f1a9013969e775c908f343e53807a03 Mon Sep 17 00:00:00 2001 From: CarlosMacaneta Date: Thu, 4 Sep 2025 09:41:01 +0200 Subject: [PATCH 4/4] build: increase app version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee48431cf77..65103d98e89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ kotlin = '2.0.20' hilt = '2.47' androidxNavigation = "2.6.0" androidxHiltNavigation = "1.0.0" -semis = "1.4.1" +semis = "1.4.2" jacoco = '0.8.10' designSystem = "0.4.0.1"