diff --git a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt index 5d177775d65..aad0dd6a98b 100644 --- a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt @@ -1,8 +1,10 @@ package com.mifos.core.data.di +import com.mifos.core.data.repository.CenterListRepository import com.mifos.core.data.repository.CheckerInboxTasksRepository import com.mifos.core.data.repository.GroupsListRepository import com.mifos.core.data.repository.NewIndividualCollectionSheetRepository +import com.mifos.core.data.repository_imp.CenterListRepositoryImp import com.mifos.core.data.repository_imp.CheckerInboxTasksRepositoryImp import com.mifos.core.data.repository_imp.GroupsListRepositoryImpl import com.mifos.core.data.repository_imp.NewIndividualCollectionSheetRepositoryImp @@ -25,4 +27,7 @@ abstract class DataModule { internal abstract fun provideGroupListRepository( groupsListRepositoryImpl: GroupsListRepositoryImpl ): GroupsListRepository + + @Binds + internal abstract fun bindCenterListRepository(impl: CenterListRepositoryImp): CenterListRepository } \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/paging_source/CenterListPagingSource.kt b/core/data/src/main/java/com/mifos/core/data/paging_source/CenterListPagingSource.kt new file mode 100644 index 00000000000..a358c55f808 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/paging_source/CenterListPagingSource.kt @@ -0,0 +1,111 @@ +package com.mifos.core.data.paging_source + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.mifos.core.network.datamanager.DataManagerCenter +import com.mifos.core.objects.client.Page +import com.mifos.core.objects.group.Center +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class CenterListPagingSource(private val dataManagerCenter: DataManagerCenter) : + PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { position -> + state.closestPageToPosition(position)?.prevKey?.plus(10) ?: state.closestPageToPosition( + position + )?.nextKey?.minus(10) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val position = params.key ?: 0 + return try { + val getCenters = getCenterList(position) + val centerList = getCenters.first + val totalCenters = getCenters.second + val centerDbList = getCenterDbList() + val centerListWithSync = getCenterListWithSync(centerList, centerDbList) + LoadResult.Page( + data = centerListWithSync, + prevKey = if (position <= 0) null else position - 10, + nextKey = if (position >= totalCenters) null else position + 10 + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + private suspend fun getCenterList(position: Int): Pair, Int> = + suspendCoroutine { continuation -> + try { + dataManagerCenter.getCenters(true, position, 10) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber>() { + override fun onCompleted() { + } + + override fun onError(exception: Throwable) { + continuation.resumeWithException(exception) + } + + override fun onNext(center: Page
) { + continuation.resume( + Pair( + center.pageItems, + center.totalFilteredRecords + ) + ) + } + }) + } catch (exception: Exception) { + continuation.resumeWithException(exception) + } + } + + private suspend fun getCenterDbList(): List
= suspendCoroutine { continuation -> + try { + + dataManagerCenter.allDatabaseCenters + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber>() { + override fun onCompleted() { + } + + override fun onError(error: Throwable) { + continuation.resumeWithException(error) + } + + override fun onNext(centers: Page
) { + continuation.resume(centers.pageItems) + } + }) + } catch (exception: Exception) { + continuation.resumeWithException(exception) + } + } + + + private fun getCenterListWithSync( + centerList: List
, + centerDbList: List
+ ): List
{ + if (centerDbList.isNotEmpty()) { + centerList.forEach { center -> + centerDbList.forEach { centerDb -> + if (center.id == centerDb.id) { + center.sync = true + } + } + } + } + return centerList + } +} \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/repository/CenterListRepository.kt b/core/data/src/main/java/com/mifos/core/data/repository/CenterListRepository.kt new file mode 100644 index 00000000000..74a95b3eaaf --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/repository/CenterListRepository.kt @@ -0,0 +1,20 @@ +package com.mifos.core.data.repository + +import androidx.paging.PagingData +import com.mifos.core.objects.client.Page +import com.mifos.core.objects.group.Center +import com.mifos.core.objects.group.CenterWithAssociations +import kotlinx.coroutines.flow.Flow +import rx.Observable + +/** + * Created by Aditya Gupta on 06/08/23. + */ +interface CenterListRepository { + + fun getAllCenters(): Flow> + + fun getCentersGroupAndMeeting(id: Int): Observable + + fun allDatabaseCenters(): Observable> +} \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/repository_imp/CenterListRepositoryImp.kt b/core/data/src/main/java/com/mifos/core/data/repository_imp/CenterListRepositoryImp.kt new file mode 100644 index 00000000000..497150e4de1 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/repository_imp/CenterListRepositoryImp.kt @@ -0,0 +1,39 @@ +package com.mifos.core.data.repository_imp + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.mifos.core.data.paging_source.CenterListPagingSource +import com.mifos.core.data.repository.CenterListRepository +import com.mifos.core.network.datamanager.DataManagerCenter +import com.mifos.core.objects.client.Page +import com.mifos.core.objects.group.Center +import com.mifos.core.objects.group.CenterWithAssociations +import kotlinx.coroutines.flow.Flow +import rx.Observable +import javax.inject.Inject + +/** + * Created by Aditya Gupta on 06/08/23. + */ +class CenterListRepositoryImp @Inject constructor(private val dataManagerCenter: DataManagerCenter) : + CenterListRepository { + + override fun getAllCenters(): Flow> { + return Pager( + config = PagingConfig( + pageSize = 10 + ), pagingSourceFactory = { + CenterListPagingSource(dataManagerCenter) + } + ).flow + } + + override fun getCentersGroupAndMeeting(id: Int): Observable { + return dataManagerCenter.getCentersGroupAndMeeting(id) + } + + override fun allDatabaseCenters(): Observable> { + return dataManagerCenter.allDatabaseCenters + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt index b1329146e2b..dacd8e7765a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt @@ -1,28 +1,30 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - package com.mifos.core.designsystem.component import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.mifos.core.designsystem.theme.White @Composable fun MifosScaffold( + modifier: Modifier = Modifier, topBar: @Composable () -> Unit, snackbarHostState: SnackbarHostState?, bottomBar: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, content: @Composable (PaddingValues) -> Unit ) { Scaffold( + modifier = modifier, topBar = topBar, snackbarHost = { snackbarHostState?.let { SnackbarHost(it) } }, containerColor = White, - bottomBar = bottomBar + bottomBar = bottomBar, + floatingActionButton = floatingActionButton ) { padding -> content(padding) } diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt new file mode 100644 index 00000000000..ba52728c9e3 --- /dev/null +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcon.kt @@ -0,0 +1,8 @@ +package com.mifos.core.designsystem.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add + +object MifosIcons { + val Add = Icons.Rounded.Add +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetCenterListDbUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetCenterListDbUseCase.kt new file mode 100644 index 00000000000..d53a3c37a03 --- /dev/null +++ b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetCenterListDbUseCase.kt @@ -0,0 +1,47 @@ +package com.mifos.core.domain.use_cases + +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.repository.CenterListRepository +import com.mifos.core.objects.client.Page +import com.mifos.core.objects.group.Center +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class GetCenterListDbUseCase @Inject constructor(private val repository: CenterListRepository) { + + suspend operator fun invoke(): Flow>> = flow { + try { + emit(Resource.Loading()) + emit(Resource.Success(getCenterList())) + } catch (exception: Exception) { + emit(Resource.Error(exception.toString())) + } + } + + private suspend fun getCenterList(): List
= suspendCoroutine { continuation -> + try { + repository.allDatabaseCenters() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber>() { + override fun onCompleted() {} + override fun onError(exception: Throwable) { + continuation.resumeWithException(exception) + } + + override fun onNext(centerPage: Page
) { + continuation.resume(centerPage.pageItems) + } + }) + } catch (exception: Exception) { + continuation.resumeWithException(exception) + } + } +} \ No newline at end of file diff --git a/feature/center/.gitignore b/feature/center/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/feature/center/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/center/build.gradle.kts b/feature/center/build.gradle.kts new file mode 100644 index 00000000000..aacf44a8f62 --- /dev/null +++ b/feature/center/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + alias(libs.plugins.mifos.android.feature) + alias(libs.plugins.mifos.android.library.compose) + alias(libs.plugins.mifos.android.library.jacoco) +} + +android { + namespace = "com.mifos.feature.center" +} + +dependencies { + + implementation(projects.core.datastore) + implementation(projects.core.network) + implementation(projects.core.domain) + + implementation(libs.androidx.material) + + //DBFlow dependencies + kapt(libs.dbflow.processor) + implementation(libs.dbflow) + kapt(libs.github.dbflow.processor) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.core.testing) + + androidTestImplementation(projects.core.testing) + + //paging compose + implementation(libs.androidx.paging.compose) + + //coil + implementation(libs.coil.kt.compose) +} \ No newline at end of file diff --git a/feature/center/consumer-rules.pro b/feature/center/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/feature/center/proguard-rules.pro b/feature/center/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/feature/center/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/center/src/androidTest/java/com/mifos/feature/center/ExampleInstrumentedTest.kt b/feature/center/src/androidTest/java/com/mifos/feature/center/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..3cad388852b --- /dev/null +++ b/feature/center/src/androidTest/java/com/mifos/feature/center/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.mifos.feature.center + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.mifos.feature.center.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/center/src/main/AndroidManifest.xml b/feature/center/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/feature/center/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListScreen.kt b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListScreen.kt new file mode 100644 index 00000000000..67086a88d0e --- /dev/null +++ b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListScreen.kt @@ -0,0 +1,579 @@ +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) + +package com.mifos.feature.center.center_list.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import coil.compose.AsyncImage +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosPagingAppendProgress +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.Black +import com.mifos.core.designsystem.theme.BlueSecondary +import com.mifos.core.designsystem.theme.DarkGray +import com.mifos.core.designsystem.theme.LightGray +import com.mifos.core.designsystem.theme.White +import com.mifos.core.objects.group.Center +import com.mifos.core.ui.components.SelectionModeTopAppBar +import com.mifos.feature.center.R +import kotlinx.coroutines.flow.flowOf + +@Composable +fun CenterListScreen( + createNewCenter: () -> Unit, + syncClicked: (List
) -> Unit, + onCenterSelect: (Center) -> Unit +) { + + val viewModel: CenterListViewModel = hiltViewModel() + val refreshState by viewModel.isRefreshing.collectAsStateWithLifecycle() + val state by viewModel.centerListUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = true) { + viewModel.getCenterList() + } + + CenterListScreen( + state = state, + createNewCenter = createNewCenter, + onRefresh = { + viewModel.refreshCenterList() + }, + refreshState = refreshState, + onCenterSelect = onCenterSelect, + syncClicked = syncClicked + ) +} + +@Composable +fun CenterListScreen( + state: CenterListUiState, + createNewCenter: () -> Unit, + onRefresh: () -> Unit, + refreshState: Boolean, + syncClicked: (List
) -> Unit, + onCenterSelect: (Center) -> Unit +) { + + val snackbarHostState = remember { SnackbarHostState() } + var isInSelectionMode by rememberSaveable { mutableStateOf(false) } + val selectedItems = remember { mutableStateListOf
() } + val resetSelectionMode = { + isInSelectionMode = false + selectedItems.clear() + } + BackHandler(enabled = isInSelectionMode) { + resetSelectionMode() + } + + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshState, + onRefresh = onRefresh + ) + + LaunchedEffect( + key1 = isInSelectionMode, + key2 = selectedItems.size, + ) { + if (isInSelectionMode && selectedItems.isEmpty()) { + isInSelectionMode = false + } + } + + MifosScaffold( + modifier = Modifier, + topBar = { + if (isInSelectionMode) { + SelectionModeTopAppBar( + itemCount = selectedItems.size, + syncClicked = { syncClicked(selectedItems.toList()) }, + resetSelectionMode = resetSelectionMode + ) + } + }, + snackbarHostState = snackbarHostState, + bottomBar = { }, + floatingActionButton = { + FloatingActionButton( + onClick = { createNewCenter() }, + containerColor = BlueSecondary + ) { + Icon( + imageVector = MifosIcons.Add, + contentDescription = null + ) + } + } + ) { paddingValue -> + Column( + modifier = Modifier + .padding(paddingValue), + verticalArrangement = Arrangement.Center + ) { + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + when (state) { + is CenterListUiState.Error -> { + MifosSweetError(message = stringResource(id = state.message)) { + onRefresh() + } + } + + is CenterListUiState.Loading -> { + MifosCircularProgress() + } + + is CenterListUiState.CenterList -> { + CenterListContent( + centerPagingList = state.centers.collectAsLazyPagingItems(), + isInSelectionMode = isInSelectionMode, + selectedItems = selectedItems, + onRefresh = { + onRefresh() + }, onCenterSelect = { + onCenterSelect(it) + }, selectedMode = { + isInSelectionMode = true + } + ) + } + + is CenterListUiState.CenterListDb -> CenterListDbContent(centerList = state.centers) + } + PullRefreshIndicator( + refreshing = refreshState, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } + } +} + +@Composable +fun CenterListContent( + centerPagingList: LazyPagingItems
, + isInSelectionMode: Boolean, + selectedItems: SnapshotStateList
, + onRefresh: () -> Unit, + onCenterSelect: (Center) -> Unit, + selectedMode: () -> Unit + +) { + + when (centerPagingList.loadState.refresh) { + is LoadState.Error -> { + MifosSweetError(message = stringResource(id = R.string.feature_center_error_loading_centers)) { + onRefresh() + } + } + + is LoadState.Loading -> MifosCircularProgress() + + is LoadState.NotLoading -> Unit + } + + LazyColumn { + items(centerPagingList.itemCount) { index -> + + val isSelected = selectedItems.contains(centerPagingList[index]) + var cardColor by remember { mutableStateOf(White) } + + OutlinedCard( + modifier = Modifier + .padding(6.dp) + .combinedClickable( + onClick = { + if (isInSelectionMode) { + cardColor = if (isSelected) { + selectedItems.remove(centerPagingList[index]) + White + } else { + centerPagingList[index]?.let { selectedItems.add(it) } + LightGray + } + } else { + centerPagingList[index]?.let { onCenterSelect(it) } + } + }, + onLongClick = { + if (isInSelectionMode) { + cardColor = if (isSelected) { + selectedItems.remove(centerPagingList[index]) + White + } else { + centerPagingList[index]?.let { selectedItems.add(it) } + LightGray + } + } else { + selectedMode() + centerPagingList[index]?.let { selectedItems.add(it) } + cardColor = LightGray + } + } + ), + colors = CardDefaults.cardColors( + containerColor = if (selectedItems.isEmpty()) { + cardColor = White + White + } else cardColor, + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 24.dp, + bottom = 24.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Canvas( + modifier = Modifier.size(16.dp), + onDraw = { + drawCircle( + color = if (centerPagingList[index]?.active == true) Color.Green else Color.Red + ) + } + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + centerPagingList[index]?.name?.let { + Text( + text = it, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = Black + ) + ) + } + Text( + text = centerPagingList[index]?.accountNo.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Row { + Text( + text = centerPagingList[index]?.officeName.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Spacer(modifier = Modifier.width(26.dp)) + Text( + text = centerPagingList[index]?.officeId.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + } + Row { + Text( + text = centerPagingList[index]?.staffName.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Spacer(modifier = Modifier.width(26.dp)) + Text( + text = centerPagingList[index]?.staffId.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + } + } + if (centerPagingList[index]?.sync == true) { + AsyncImage( + modifier = Modifier.size(20.dp), + model = R.drawable.feature_center_ic_done_all_black_24dp, + contentDescription = null + ) + } + } + } + } + + when (centerPagingList.loadState.append) { + is LoadState.Error -> {} + + is LoadState.Loading -> { + item { + MifosPagingAppendProgress() + } + } + + is LoadState.NotLoading -> Unit + } + when (centerPagingList.loadState.append.endOfPaginationReached) { + true -> { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(6.dp), + text = stringResource(id = R.string.feature_center_no_more_centers), + style = TextStyle( + fontSize = 14.sp + ), + color = DarkGray, + textAlign = TextAlign.Center + ) + } + } + + false -> Unit + } + } +} + +@Composable +fun CenterListDbContent( + centerList: List
+) { + LazyColumn { + items(centerList) { center -> + + OutlinedCard( + modifier = Modifier + .padding(6.dp), + colors = CardDefaults.cardColors( + containerColor = White + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 24.dp, + bottom = 24.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Canvas( + modifier = Modifier.size(16.dp), + onDraw = { + drawCircle( + color = if (center.active == true) Color.Green else Color.Red + ) + } + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + center.name?.let { + Text( + text = it, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = Black + ) + ) + } + Text( + text = center.accountNo.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Row { + Text( + text = center.officeName.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Spacer(modifier = Modifier.width(26.dp)) + Text( + text = center.officeId.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + } + Row { + Text( + text = center.staffName.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + Spacer(modifier = Modifier.width(26.dp)) + Text( + text = center.staffId.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = DarkGray + ) + ) + } + } + AsyncImage( + modifier = Modifier.size(20.dp), + model = R.drawable.feature_center_ic_done_all_black_24dp, + contentDescription = null + ) + } + } + } + } +} + + +class CenterListUiStateProvider : + PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + CenterListUiState.Loading, + CenterListUiState.Error(R.string.feature_center_error_loading_centers), + CenterListUiState.CenterListDb(sampleCenterListDb), + CenterListUiState.CenterList(sampleCenterList) + ) +} + + +@Preview(showBackground = true) +@Composable +private fun CenterListContentPreview() { + CenterListContent( + centerPagingList = sampleCenterList.collectAsLazyPagingItems(), + isInSelectionMode = false, + selectedItems = rememberSaveable { mutableStateListOf() }, + onRefresh = {}, + onCenterSelect = {}, + selectedMode = {} + ) +} + + +@Preview(showBackground = true) +@Composable +private fun CenterListDbContentPreview() { + CenterListDbContent(sampleCenterListDb) +} + +@Preview(showBackground = true) +@Composable +private fun CenterListScreenPreview( + @PreviewParameter(CenterListUiStateProvider::class) centerListUiState: CenterListUiState +) { + CenterListScreen( + state = centerListUiState, + createNewCenter = {}, + onRefresh = {}, + refreshState = false, + onCenterSelect = {}, + syncClicked = {} + ) +} + +val sampleCenterListDb = List(10) { + Center( + name = "Center $it", + officeId = it, + officeName = "Office $it", + staffId = it, + staffName = "Staff $it", + active = it % 2 == 0 + ) +} + +val sampleCenterList = flowOf(PagingData.from(sampleCenterListDb)) diff --git a/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListViewModel.kt b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListViewModel.kt new file mode 100644 index 00000000000..a5ebf3b7ff8 --- /dev/null +++ b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterListViewModel.kt @@ -0,0 +1,61 @@ +package com.mifos.feature.center.center_list.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.repository.CenterListRepository +import com.mifos.core.datastore.PrefManager +import com.mifos.core.domain.use_cases.GetCenterListDbUseCase +import com.mifos.feature.center.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class CenterListViewModel @Inject constructor( + private val prefManager: PrefManager, + private val repository: CenterListRepository, + private val getCenterListDbUseCase: GetCenterListDbUseCase +) : ViewModel() { + + // for refresh feature + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + fun refreshCenterList() { + _isRefreshing.value = true + getCenterList() + _isRefreshing.value = false + } + + private val _centerListUiState = MutableStateFlow(CenterListUiState.Loading) + val centerListUiState = _centerListUiState.asStateFlow() + + fun getCenterList() { + if (prefManager.userStatus) loadCentersFromDb() + else loadCentersFromApi() + } + + private fun loadCentersFromApi() = viewModelScope.launch(Dispatchers.IO) { + val response = repository.getAllCenters() + _centerListUiState.value = CenterListUiState.CenterList(response) + } + + private fun loadCentersFromDb() = viewModelScope.launch(Dispatchers.IO) { + getCenterListDbUseCase().collect { result -> + when (result) { + is Resource.Error -> _centerListUiState.value = + CenterListUiState.Error(R.string.feature_center_failed_to_load_db_centers) + + is Resource.Loading -> _centerListUiState.value = CenterListUiState.Loading + + is Resource.Success -> _centerListUiState.value = + CenterListUiState.CenterListDb(result.data ?: emptyList()) + } + } + } +} diff --git a/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterUiState.kt b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterUiState.kt new file mode 100644 index 00000000000..f044e5cf9a0 --- /dev/null +++ b/feature/center/src/main/java/com/mifos/feature/center/center_list/ui/CenterUiState.kt @@ -0,0 +1,20 @@ +package com.mifos.feature.center.center_list.ui + +import androidx.paging.PagingData +import com.mifos.core.objects.group.Center +import com.mifos.core.objects.group.CenterWithAssociations +import kotlinx.coroutines.flow.Flow + +/** + * Created by Aditya Gupta on 06/08/23. + */ +sealed class CenterListUiState { + + data object Loading : CenterListUiState() + + data class Error(val message: Int) : CenterListUiState() + + data class CenterList(val centers: Flow>) : CenterListUiState() + + data class CenterListDb(val centers: List
) : CenterListUiState() +} \ No newline at end of file diff --git a/feature/center/src/main/res/drawable/feature_center_ic_done_all_black_24dp.xml b/feature/center/src/main/res/drawable/feature_center_ic_done_all_black_24dp.xml new file mode 100644 index 00000000000..3ff8c7dd868 --- /dev/null +++ b/feature/center/src/main/res/drawable/feature_center_ic_done_all_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/center/src/main/res/values/strings.xml b/feature/center/src/main/res/values/strings.xml new file mode 100644 index 00000000000..3ee4a26d3e8 --- /dev/null +++ b/feature/center/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Failed to load Centers + Failed to load Database Centers + No More Centers Available ! + \ No newline at end of file diff --git a/feature/center/src/test/java/com/mifos/feature/center/ExampleUnitTest.kt b/feature/center/src/test/java/com/mifos/feature/center/ExampleUnitTest.kt new file mode 100644 index 00000000000..1346bd44160 --- /dev/null +++ b/feature/center/src/test/java/com/mifos/feature/center/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.mifos.feature.center + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/feature/collection-sheet/src/main/java/com/mifos/feature/individual_collection_sheet/individual_collection_sheet/ui/IndividualCollectionSheetScreen.kt b/feature/collection-sheet/src/main/java/com/mifos/feature/individual_collection_sheet/individual_collection_sheet/ui/IndividualCollectionSheetScreen.kt index 5dc8fcadb23..d1fea855731 100644 --- a/feature/collection-sheet/src/main/java/com/mifos/feature/individual_collection_sheet/individual_collection_sheet/ui/IndividualCollectionSheetScreen.kt +++ b/feature/collection-sheet/src/main/java/com/mifos/feature/individual_collection_sheet/individual_collection_sheet/ui/IndividualCollectionSheetScreen.kt @@ -87,8 +87,7 @@ fun IndividualCollectionSheetScreen( } ) }, - snackbarHostState = snackbarHostState, - bottomBar = { } + snackbarHostState = snackbarHostState ) { paddingValues -> Column( modifier = Modifier.padding(paddingValues) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83c2d7c5706..b4b71f52a3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ androidxMacroBenchmark = "1.2.3" androidxBaselineProfile = "1.2.3" androidxBenchmark = "1.2.3" androidxBenchmarkJunit4 = "1.1.1" +androidxMaterial = "1.6.7" androidxMetrics = "1.0.0-beta01" androidxNavigation = "2.7.7" androidxProfileinstaller = "1.3.1" @@ -78,7 +79,7 @@ leakcanaryVersion = '2.10' material = "1.11.0" materialIconsExtended = "1.6.2" materialshowcaseview = "1.3.7" -materialVersion = "1.6.2" +materialVersion = "1.6.7" mifosAndroidSdkArchVersion = "1.06" mifosPasscode = "1.0.0" mockitoCore = "5.5.0" diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 820d54c14f9..1a514e5017e 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -133,6 +133,7 @@ dependencies { implementation(projects.feature.collectionSheet) implementation(projects.feature.groups) implementation(projects.feature.settings) + implementation(projects.feature.center) implementation(projects.core.common) implementation(projects.core.ui) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/centerlist/CenterListFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/centerlist/CenterListFragment.kt index ff8f44e0e1a..11e92708fd5 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/centerlist/CenterListFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/centerlist/CenterListFragment.kt @@ -6,399 +6,75 @@ package com.mifos.mifosxdroid.online.centerlist import android.os.Bundle import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.Button -import androidx.appcompat.view.ActionMode -import androidx.lifecycle.ViewModelProvider +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler import com.mifos.core.objects.group.Center -import com.mifos.core.objects.group.CenterWithAssociations +import com.mifos.feature.center.center_list.ui.CenterListScreen import com.mifos.mifosxdroid.R import com.mifos.mifosxdroid.activity.home.HomeActivity -import com.mifos.mifosxdroid.adapters.CentersListAdapter -import com.mifos.mifosxdroid.core.EndlessRecyclerViewScrollListener -import com.mifos.mifosxdroid.core.MifosBaseActivity import com.mifos.mifosxdroid.core.MifosBaseFragment -import com.mifos.mifosxdroid.core.util.Toaster -import com.mifos.mifosxdroid.databinding.FragmentCentersListBinding import com.mifos.mifosxdroid.dialogfragments.synccenterdialog.SyncCentersDialogFragment -import com.mifos.mifosxdroid.online.collectionsheet.CollectionSheetFragment -import com.mifos.mifosxdroid.uihelpers.MFDatePicker import com.mifos.utils.FragmentConstants import dagger.hilt.android.AndroidEntryPoint /** * Created by ishankhanna on 11/03/14. - * - * - * CenterListFragment Fetching and Showing CenterList in RecyclerView from - * >demo.openmf.org/fineract-provider/api/v1/centers?paged=true&offset=0&limit=100> - */ + **/ @AndroidEntryPoint -class CenterListFragment : MifosBaseFragment(), OnRefreshListener { - - private lateinit var binding: FragmentCentersListBinding - - private lateinit var viewModel: CenterListViewModel - - lateinit var centersListAdapter: CentersListAdapter - private lateinit var centers: List
- private var selectedCenters: MutableList
? = null - private lateinit var layoutManager: LinearLayoutManager - private lateinit var actionModeCallback: ActionModeCallback - private var actionMode: ActionMode? = null - private lateinit var sweetUIErrorHandler: SweetUIErrorHandler - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - centers = ArrayList() - selectedCenters = ArrayList() - actionModeCallback = ActionModeCallback() - } +class CenterListFragment : MifosBaseFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentCentersListBinding.inflate(inflater, container, false) -// mCenterListPresenter.attachView(this) - viewModel = ViewModelProvider(this)[CenterListViewModel::class.java] - - //Showing User Interface. - showUserInterface() - - //Fetching Centers - /** - * This is the LoadMore of the RecyclerView. It called When Last Element of RecyclerView - * will shown on the Screen. - */ - binding.rvCenterList.addOnScrollListener(object : - EndlessRecyclerViewScrollListener(layoutManager) { - override fun onLoadMore(page: Int, totalItemsCount: Int) { - viewModel.loadCenters(true, totalItemsCount) - } - }) - viewModel.loadCenters(false, 0) - viewModel.loadDatabaseCenters() - return binding.root - } - - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.centerListUiState.observe(viewLifecycleOwner) { - when (it) { - is CenterListUiState.ShowCenters -> { - showProgressbar(false) - showCenters(it.centers) - } - - is CenterListUiState.ShowCentersGroupAndMeeting -> { - showProgressbar(false) - showCentersGroupAndMeeting(it.centerWithAssociations, it.id) - } - - is CenterListUiState.ShowEmptyCenters -> { - showProgressbar(false) - showEmptyCenters(it.message) - } - - CenterListUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError() - } - - is CenterListUiState.ShowMessage -> { - showProgressbar(false) - showMessage(it.message) - } - - is CenterListUiState.ShowMoreCenters -> { - showProgressbar(false) - showMoreCenters(it.centers) - } - - is CenterListUiState.ShowProgressbar -> showProgressbar(true) - CenterListUiState.UnregisterSwipeAndScrollListener -> { - showProgressbar(false) - unregisterSwipeAndScrollListener() - } - } - } - - binding.fabCreateCenter.setOnClickListener { - onClickCreateNewCenter() - } - binding.layoutError.findViewById