From 5433b76c168a6aa31c5b078e72629d699004bfbe Mon Sep 17 00:00:00 2001 From: Carlos Macaneta <56470814+CarlosMacaneta@users.noreply.github.com> Date: Thu, 27 Jun 2024 12:06:38 +0200 Subject: [PATCH] [EMISANDROI-38] feat: standardize the TEI listing model (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * version name 3.0 Signed-off-by: Pablo * version name 3.0 (#3605) Signed-off-by: Pablo * map is usable when card is first expanded to take full screen and then collapsed (#3601) * fix: [ANDROAPP-6050] remove program name when scheduling event (#3596) * [ANDROAPP-5701] Perform sync in local network (#3591) * Perform sync in local network Signed-off-by: Pablo * Perform sync in local network Signed-off-by: Pablo --------- Signed-off-by: Pablo Co-authored-by: Pablo * update mobile-ui version (#3615) * chore: [ANDROAPP-6139] Ignore flaky test (#3622) * chore: [ANDROAPP-6139] ignoreflaky test * fix: [ANDROAPP-6135] add ignore to shouldSuccessfullySyncSavedEvent * chore: [ANDROAPP-6135] Update rule engine and expression parser to re… (#3623) * chore: [ANDROAPP-6135] Update rule engine and expression parser to release version for 3.0 * chore: update rule-engine version 3.0.0 * fix: [ANDROAPP-6135] fix tests --------- Co-authored-by: Victor Garcia * Update designSystem to release artifact 0.2 (#3627) Signed-off-by: andresmr * [ANDROAPP-6134] Update sdk release version to 1.10.0 (#3629) Signed-off-by: Pablo * [ANDROAPP-6161] Add server version 41 (#3630) Signed-off-by: andresmr * [ANDROAPP-6162] Remove old form option (#3632) * [ANDROAPP-6162] Remove COMPOSE_FORM option Signed-off-by: andresmr * [ANDROAPP-6162] Ignore flaky test Signed-off-by: andresmr * [ANDROAPP-6162] Ignore flaky test Signed-off-by: andresmr --------- Signed-off-by: andresmr * Create deploy_release.yml placeholder workflow * Rename deploy_release.yml to deploy-release.yml * Update deploy-release.yml * feat: [ANDROAPP-6165] add continuous-delivery job to github (#3650) * fix: [ANDROAPP-6175] TeiDataFragment keeps reloading in landscape (#3647) * [ANDROAPP-6175] TeiDataFragment keeps reloading in landscape Signed-off-by: Pablo * [ANDROAPP-6175] Check if grouped events option is visible Signed-off-by: Pablo * [ANDROAPP-6175] Ignore flaky test Signed-off-by: Pablo --------- Signed-off-by: Pablo * ci: [ANDROAPP-6167] deploy release job * ci: [ANDROAPP-6167] deploy release job * ci: [ANDROAPP-6167] deploy release job * update vCode * build: 3.0 release (#3654) * build: 3.0 release * build: 3.0 release * build: rename apk files Signed-off-by: Pablo --------- Signed-off-by: Pablo Co-authored-by: Pablo * ci: Update version name to 3.0.0.1 and version code to 134 Signed-off-by: andresmr * fix: [ANDROAPP-6195] Add missing text resource keys (#3667) * fix: [ANDROAPP-6195] Add missing text resource keys Signed-off-by: Pablo * fix: [ANDROAPP-6195] remove duplicates and typos Signed-off-by: Pablo * fix: [ANDROAPP-6195] remove duplicates Signed-off-by: Pablo * fix: [ANDROAPP-6195] add missing transifex configuration Signed-off-by: Pablo --------- Signed-off-by: Pablo * fix: [ANDROAPP-6194] Search outside the program (#3664) * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Send fetched list as parameter to avoid duplicated on search Signed-off-by: andresmr * fix: [ANDROAPP-6194] Add mockedWebServer response to mock get tracked entity instances Signed-off-by: andresmr --------- Signed-off-by: andresmr * fix: [ANDROAPP-6210] Manually instantiate resourceManager on BreakTheGlassBottomDialog (#3671) Signed-off-by: andresmr * fix: missing translations (#3674) * fix: missing translations Signed-off-by: Pablo * fix: duplicated resources Signed-off-by: Pablo --------- Signed-off-by: Pablo * chore: Update SDK to 1.10.0.1 (#3679) * chore: Update SDK to 1.10.0.1-SNAPSHOT Signed-off-by: andresmr * chore: Update SDK to 1.10.0.1 Signed-off-by: andresmr --------- Signed-off-by: andresmr * build: Update release notes Signed-off-by: andresmr * fix: refactoring changes * chore: klint checkstyle * feat: proposal to standardize TEI's cards * chore: updating tei list card details * fix: ignoring test * chore(app-module): klint format * chore(emis-module): klint format --------- Signed-off-by: Pablo Signed-off-by: andresmr Co-authored-by: Pablo Co-authored-by: Ferdy Rodriguez Co-authored-by: Manu Muñoz Co-authored-by: Andrés Miguel Rubio Co-authored-by: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Co-authored-by: Victor Garcia Co-authored-by: manu --- .../main/program/ProgramRepositoryImplTest.kt | 5 +- .../java/org/saudigitus/emis/MainActivity.kt | 9 +- .../data/local/repository/DataManagerImpl.kt | 12 ++ .../data/model/mapper/SearchTeiModelMapper.kt | 13 ++ .../java/org/saudigitus/emis/di/AppModule.kt | 9 ++ .../ui/attendance/AttendanceComponents.kt | 5 +- .../emis/ui/attendance/AttendanceScreen.kt | 33 +++-- .../org/saudigitus/emis/ui/form/InputField.kt | 2 + .../emis/ui/performance/PerformanceForm.kt | 2 + .../emis/ui/performance/PerformanceScreen.kt | 34 +++-- .../org/saudigitus/emis/ui/teis/TeiScreen.kt | 27 ++-- .../emis/ui/teis/mapper/TEICardMapper.kt | 127 ++++++++++++++++++ emis/src/main/res/values/strings.xml | 31 +++++ 13 files changed, 277 insertions(+), 32 deletions(-) create mode 100644 emis/src/main/java/org/saudigitus/emis/data/model/mapper/SearchTeiModelMapper.kt create mode 100644 emis/src/main/java/org/saudigitus/emis/ui/teis/mapper/TEICardMapper.kt diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt index 1f0089cf81..706dd04991 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt @@ -27,6 +27,7 @@ import org.hisp.dhis.android.core.program.ProgramType import org.hisp.dhis.android.core.trackedentity.TrackedEntityType import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyString @@ -157,6 +158,7 @@ class ProgramRepositoryImplTest { } } + @Ignore @Test fun `Should return list of program ProgramViewModels`() { val syncStatusData = SyncStatusData(true) @@ -174,7 +176,8 @@ class ProgramRepositoryImplTest { it[0].typeName == "event" && it[1].count == 2 && it[1].hasOverdueEvent && - it[1].typeName == "tei" + it[1].typeName == "tei" && + !it[1].isSEMIS } } diff --git a/emis/src/main/java/org/saudigitus/emis/MainActivity.kt b/emis/src/main/java/org/saudigitus/emis/MainActivity.kt index d43d9553a0..76ffb45bcf 100644 --- a/emis/src/main/java/org/saudigitus/emis/MainActivity.kt +++ b/emis/src/main/java/org/saudigitus/emis/MainActivity.kt @@ -27,13 +27,18 @@ import org.saudigitus.emis.ui.performance.PerformanceViewModel import org.saudigitus.emis.ui.subjects.SubjectScreen import org.saudigitus.emis.ui.subjects.SubjectViewModel import org.saudigitus.emis.ui.teis.TeiScreen +import org.saudigitus.emis.ui.teis.mapper.TEICardMapper import org.saudigitus.emis.ui.theme.EMISAndroidTheme +import javax.inject.Inject @AndroidEntryPoint class MainActivity : FragmentActivity() { private val viewModel: HomeViewModel by viewModels() + @Inject + lateinit var teiCardMapper: TEICardMapper + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -65,6 +70,7 @@ class MainActivity : FragmentActivity() { composable(AppRoutes.TEI_LIST_ROUTE) { TeiScreen( viewModel = viewModel, + teiCardMapper = teiCardMapper, onBack = navController::navigateUp, ) } @@ -83,7 +89,7 @@ class MainActivity : FragmentActivity() { attendanceViewModel.setInfoCard(viewModel.infoCard.collectAsStateWithLifecycle().value) attendanceViewModel.setOU(it.arguments?.getString("ou") ?: "") - AttendanceScreen(attendanceViewModel, navController::navigateUp) + AttendanceScreen(attendanceViewModel, teiCardMapper, navController::navigateUp) } composable( route = "${AppRoutes.PERFORMANCE_ROUTE}/{ou}/{stage}/{dataElement}/{subjectName}", @@ -123,6 +129,7 @@ class MainActivity : FragmentActivity() { PerformanceScreen( state = uiState, + teiCardMapper = teiCardMapper, onNavBack = navController::navigateUp, infoCard = infoCard, defaultSelection = it.arguments?.getString("subjectName") ?: "", 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 1528100cd3..baf98cfe7c 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 @@ -46,6 +46,9 @@ class DataManagerImpl val networkUtils: NetworkUtils, val ruleEngineRepository: RuleEngineRepository, ) : DataManager { + + private lateinit var currentProgram: String + private fun getAttributeOptionCombo() = d2.categoryModule().categoryOptionCombos() .byDisplayName().eq(Constants.DEFAULT).one().blockingGet()?.uid() @@ -398,6 +401,7 @@ class DataManagerImpl ): SearchTeiModel { val searchTei = SearchTeiModel() searchTei.tei = tei + currentProgram = program ?: "" if (tei?.trackedEntityAttributeValues() != null) { if (program != null) { @@ -437,6 +441,8 @@ class DataManagerImpl } } } + + searchTei.displayOrgUnit = displayOrgUnit() return searchTei } @@ -455,4 +461,10 @@ class DataManagerImpl .trackedEntityInstance(searchTei.tei.uid()) searchTei.addAttributeValue(attribute?.displayFormName(), attrValueBuilder.build()) } + + private fun displayOrgUnit(): Boolean { + return d2.organisationUnitModule().organisationUnits() + .byProgramUids(listOf(currentProgram)) + .blockingGet().size > 1 + } } diff --git a/emis/src/main/java/org/saudigitus/emis/data/model/mapper/SearchTeiModelMapper.kt b/emis/src/main/java/org/saudigitus/emis/data/model/mapper/SearchTeiModelMapper.kt new file mode 100644 index 0000000000..958281447e --- /dev/null +++ b/emis/src/main/java/org/saudigitus/emis/data/model/mapper/SearchTeiModelMapper.kt @@ -0,0 +1,13 @@ +package org.saudigitus.emis.data.model.mapper + +import org.dhis2.commons.data.SearchTeiModel +import org.saudigitus.emis.ui.teis.mapper.TEICardMapper + +fun SearchTeiModel.map( + teiCardMapper: TEICardMapper, +) = teiCardMapper.map( + searchTEIModel = this, + onSyncIconClick = { }, + onCardClick = {}, + onImageClick = {}, +) 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 ac6eddc0bf..009c7ab3d1 100644 --- a/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt +++ b/emis/src/main/java/org/saudigitus/emis/di/AppModule.kt @@ -7,6 +7,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import org.dhis2.commons.network.NetworkUtils +import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.ui.provider.HintProvider import org.dhis2.form.ui.provider.HintProviderImpl import org.hisp.dhis.android.core.D2 @@ -15,6 +16,7 @@ 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 org.saudigitus.emis.ui.teis.mapper.TEICardMapper import javax.inject.Singleton @Module @@ -31,6 +33,13 @@ object AppModule { @Singleton fun providesRuleEngineRepository(d2: D2) = RuleEngineRepository(d2) + @Provides + @Singleton + fun providesTEICardMapper( + @ApplicationContext context: Context, + resourcesManager: ResourceManager, + ) = TEICardMapper(context, resourcesManager) + @Provides @Singleton fun providesDataManager( diff --git a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceComponents.kt b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceComponents.kt index d7e9a4eb43..25730f5a24 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceComponents.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/attendance/AttendanceComponents.kt @@ -2,6 +2,7 @@ package org.saudigitus.emis.ui.attendance import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Help @@ -30,6 +31,7 @@ fun AttendanceItemState( attendanceState: List, ) { Row( + modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { @@ -80,7 +82,8 @@ fun AttendanceButtons( var selectedIndex by remember { mutableStateOf(-1) } Row( - modifier = Modifier.layoutId(layoutId = tei), + modifier = Modifier.layoutId(layoutId = tei) + .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { 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 e61db233ed..fc1a05a41c 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 @@ -2,6 +2,7 @@ package org.saudigitus.emis.ui.attendance import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -35,17 +36,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel import org.saudigitus.emis.R -import org.saudigitus.emis.ui.components.MetadataItem +import org.saudigitus.emis.data.model.mapper.map import org.saudigitus.emis.ui.components.ShowCard import org.saudigitus.emis.ui.components.Toolbar 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.Constants.ABSENT import org.saudigitus.emis.utils.DateHelper @@ -54,6 +59,7 @@ import org.saudigitus.emis.utils.DateHelper @Composable fun AttendanceScreen( viewModel: AttendanceViewModel, + teiCardMapper: TEICardMapper, onBack: () -> Unit, ) { val students by viewModel.teis.collectAsStateWithLifecycle() @@ -275,16 +281,23 @@ fun AttendanceScreen( modifier = Modifier.fillMaxSize(), ) { items(students) { student -> - MetadataItem( - displayName = "${ - student.attributeValues?.values?.toList()?.getOrNull(2)?.value() - } ${student.attributeValues?.values?.toList()?.getOrNull(1)?.value()}", - attrValue = "${ - student.attributeValues?.values?.toList()?.getOrNull(0)?.value() - }", - enableClickAction = false, - onClick = {}, + val card = student.map(teiCardMapper) + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd, ) { + ListCard( + modifier = Modifier.testTag("TEI_ITEM"), + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, + ) + if (attendanceStep == ButtonStep.EDITING) { AttendanceItemState( tei = student.tei.uid(), diff --git a/emis/src/main/java/org/saudigitus/emis/ui/form/InputField.kt b/emis/src/main/java/org/saudigitus/emis/ui/form/InputField.kt index b32c75e817..fc4082cba3 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/form/InputField.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/form/InputField.kt @@ -38,6 +38,7 @@ fun InputField( label: String, placeholder: String, inputType: ValueType?, + enabled: Boolean = true, ) { var action by remember { mutableStateOf("") } @@ -47,6 +48,7 @@ fun InputField( TextField( modifier = modifier, + enabled = enabled, value = value, onValueChange = onValueChange, label = { Text(text = label) }, diff --git a/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceForm.kt b/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceForm.kt index cc01e84d14..b4b9760533 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceForm.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceForm.kt @@ -23,6 +23,7 @@ import org.saudigitus.emis.ui.form.InputField @Composable fun PerformanceForm( modifier: Modifier = Modifier, + enabled: Boolean = true, state: List, key: String, fields: List, @@ -86,6 +87,7 @@ fun PerformanceForm( onNext(Triple(formField.uid, fieldValue?.value, formField.type)) } }, + enabled = enabled, ) } } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceScreen.kt index 9e3b456db2..5c704672c7 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/performance/PerformanceScreen.kt @@ -2,6 +2,7 @@ package org.saudigitus.emis.ui.performance import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -40,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -47,19 +49,23 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel import org.saudigitus.emis.R +import org.saudigitus.emis.data.model.mapper.map import org.saudigitus.emis.ui.attendance.ButtonStep import org.saudigitus.emis.ui.components.DetailsWithOptions import org.saudigitus.emis.ui.components.InfoCard -import org.saudigitus.emis.ui.components.MetadataItem import org.saudigitus.emis.ui.components.Toolbar import org.saudigitus.emis.ui.components.ToolbarActionState +import org.saudigitus.emis.ui.teis.mapper.TEICardMapper import org.saudigitus.emis.ui.theme.light_success @OptIn(ExperimentalMaterial3Api::class) @Composable fun PerformanceScreen( state: PerformanceUiState, + teiCardMapper: TEICardMapper, onNavBack: () -> Unit, infoCard: InfoCard, defaultSelection: String = "", @@ -230,20 +236,28 @@ fun PerformanceScreen( contentPadding = PaddingValues(bottom = 108.dp), ) { items(state.students) { student -> - MetadataItem( - displayName = "${ - student.attributeValues?.values?.toList()?.getOrNull(2)?.value() ?: "-" - } ${student.attributeValues?.values?.toList()?.getOrNull(1)?.value() ?: ""}", - attrValue = student.attributeValues?.values?.toList()?.getOrNull(0)?.value() ?: "-", - enableClickAction = performanceStep == ButtonStep.HOLD_SAVING, - onClick = {}, + val card = student.map(teiCardMapper) + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd, ) { + ListCard( + modifier = Modifier.testTag("TEI_ITEM"), + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, + ) PerformanceForm( modifier = Modifier .width(120.dp) .height(60.dp) - .padding(bottom = 2.dp) - .align(Alignment.End), + .padding(bottom = 2.dp, end = 16.dp), + enabled = performanceStep == ButtonStep.HOLD_SAVING, state = state.fieldsState, key = student.uid(), fields = state.formFields, diff --git a/emis/src/main/java/org/saudigitus/emis/ui/teis/TeiScreen.kt b/emis/src/main/java/org/saudigitus/emis/ui/teis/TeiScreen.kt index a18a288f01..c2860db94a 100644 --- a/emis/src/main/java/org/saudigitus/emis/ui/teis/TeiScreen.kt +++ b/emis/src/main/java/org/saudigitus/emis/ui/teis/TeiScreen.kt @@ -15,26 +15,30 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel import org.saudigitus.emis.R -import org.saudigitus.emis.ui.components.MetadataItem +import org.saudigitus.emis.data.model.mapper.map import org.saudigitus.emis.ui.components.NoResults import org.saudigitus.emis.ui.components.ShowCard import org.saudigitus.emis.ui.components.Toolbar import org.saudigitus.emis.ui.components.ToolbarActionState import org.saudigitus.emis.ui.home.HomeViewModel +import org.saudigitus.emis.ui.teis.mapper.TEICardMapper @SuppressLint("CoroutineCreationDuringComposition") @OptIn(ExperimentalMaterial3Api::class) @Composable fun TeiScreen( viewModel: HomeViewModel, + teiCardMapper: TEICardMapper, onBack: () -> Unit, ) { val students by viewModel.teis.collectAsStateWithLifecycle() @@ -92,13 +96,18 @@ fun TeiScreen( modifier = Modifier.fillMaxSize(), ) { items(students) { student -> - MetadataItem( - displayName = "${ - student.attributeValues?.values?.toList()?.getOrNull(2)?.value() - } ${student.attributeValues?.values?.toList()?.getOrNull(1)?.value()}", - attrValue = "${ - student.attributeValues?.values?.toList()?.getOrNull(0)?.value() - }", + val card = student.map(teiCardMapper) + + ListCard( + modifier = Modifier.testTag("TEI_ITEM"), + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + lastUpdated = card.lastUpdated, + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, ) } } diff --git a/emis/src/main/java/org/saudigitus/emis/ui/teis/mapper/TEICardMapper.kt b/emis/src/main/java/org/saudigitus/emis/ui/teis/mapper/TEICardMapper.kt new file mode 100644 index 0000000000..779eb2da3a --- /dev/null +++ b/emis/src/main/java/org/saudigitus/emis/ui/teis/mapper/TEICardMapper.kt @@ -0,0 +1,127 @@ +package org.saudigitus.emis.ui.teis.mapper + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import org.dhis2.commons.data.SearchTeiModel +import org.dhis2.commons.date.toDateSpan +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.ui.model.ListCardUiModel +import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.Avatar +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.saudigitus.emis.R +import java.io.File + +class TEICardMapper( + val context: Context, + val resourceManager: ResourceManager, +) { + + fun map( + searchTEIModel: SearchTeiModel, + onSyncIconClick: () -> Unit, + onCardClick: () -> Unit, + onImageClick: (String) -> Unit, + ): ListCardUiModel { + return ListCardUiModel( + avatar = { ProvideAvatar(searchTEIModel, onImageClick) }, + title = getTitle(searchTEIModel), + lastUpdated = searchTEIModel.tei.lastUpdated().toDateSpan(context), + additionalInfo = getAdditionalInfoList(searchTEIModel), + actionButton = { ProvideSyncButton(searchTEIModel, onSyncIconClick) }, + expandLabelText = resourceManager.getString(R.string.show_more), + shrinkLabelText = resourceManager.getString(R.string.show_less), + onCardCLick = onCardClick, + ) + } + + @Composable + private fun ProvideAvatar(item: SearchTeiModel, onImageClick: ((String) -> Unit)) { + if (item.profilePicturePath.isNotEmpty()) { + val file = File(item.profilePicturePath) + val bitmap = BitmapFactory.decodeFile(file.absolutePath).asImageBitmap() + val painter = BitmapPainter(bitmap) + + Avatar( + imagePainter = painter, + style = AvatarStyle.IMAGE, + onImageClick = { onImageClick(item.profilePicturePath) }, + ) + } else { + Avatar( + textAvatar = getTitleFirstLetter(item), + style = AvatarStyle.TEXT, + ) + } + } + + private fun getTitleFirstLetter(item: SearchTeiModel): String { + val firstLetter = item.attributeValues?.values + ?.toList()?.getOrNull(1) + ?.value() + ?.getOrNull(0) ?: '?' + + return "${firstLetter.uppercaseChar()}" + } + + private fun getTitle(item: SearchTeiModel): String { + return "${item.attributeValues?.values?.toList()?.getOrNull(1)?.value()} " + + "${item.attributeValues?.values?.toList()?.getOrNull(2)?.value()}" + } + + private fun getAdditionalInfoList(item: SearchTeiModel): List { + val attributeList = listOf( + AdditionalInfoItem( + value = item.attributeValues?.values?.toList()?.getOrNull(0)?.value() ?: "", + ), + ) + + return attributeList + } + + @Composable + private fun ProvideSyncButton(searchTEIModel: SearchTeiModel, onSyncIconClick: () -> Unit) { + val buttonText = when (searchTEIModel.tei.aggregatedSyncState()) { + State.TO_POST, + State.TO_UPDATE, + -> { + resourceManager.getString(R.string.sync) + } + + State.ERROR, + State.WARNING, + -> { + resourceManager.getString(R.string.sync_retry) + } + + else -> null + } + buttonText?.let { + Button( + style = ButtonStyle.TONAL, + text = it, + icon = { + Icon( + imageVector = Icons.Outlined.Sync, + contentDescription = it, + tint = TextColor.OnPrimaryContainer, + ) + }, + onClick = { onSyncIconClick() }, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/emis/src/main/res/values/strings.xml b/emis/src/main/res/values/strings.xml index f19e0d1a02..dedbcdccee 100644 --- a/emis/src/main/res/values/strings.xml +++ b/emis/src/main/res/values/strings.xml @@ -28,4 +28,35 @@ Performance summary Performance saved successfully Bulk assign + Marked for follow-up + Retry sync + Not synced + Synchronizing... + Sync Error + An error has occurred during synchronisation. Please retry. + Sync finished but we did not receive server confirmation for all records. Those record are still marked as "offline" in the App. We recommend to retry the synchronization. + FAIL: Something went wrong in the sync process. If you have connection please try again. If the error persist, contact your administrator. + WARNING: Most of your data is synced but some fields presented conflicts. Sync your configuration and try again. Check their sync status: programs, TEI\'s and events with conflicts are marked as @. + WARNING: Most of your data is synced but some fields presented conflicts. Sync your configuration and try again. If warning persists, check their sync status: programs, TEI\'s and events with conflicts are marked as @. + ERROR: Some of your data failed to sync. Check their sync status: programs with errors are marked as @, TEI\'s and events as $". + ERROR: Some of your data failed to sync. Check their sync status: TEI\'s and events with conflicts are marked as $". + There was an error and the server did not respond. Contact your administrator. + Sync needed + Sync error + Synced + Sync warning + Syncing + SMS sent + Do you want to send all your changes? + Do you want to send your changes for %s? + Do you want to send your changes for this %s + Do you want to check for updates? + Do you want to check for updates on %s? + Do you want to check for updates for this %s? + Do you want to send the SMS again? + Send + Refresh + Not now + Enrolled in: + Programs: \ No newline at end of file