diff --git a/cmp-navigation/build.gradle.kts b/cmp-navigation/build.gradle.kts index de946e29c91..1ec95aa98db 100644 --- a/cmp-navigation/build.gradle.kts +++ b/cmp-navigation/build.gradle.kts @@ -39,7 +39,7 @@ kotlin { implementation(projects.feature.collectionSheet) // implementation(projects.feature.dataTable) // implementation(projects.feature.document) -// implementation(projects.feature.groups) + implementation(projects.feature.groups) // implementation(projects.feature.loan) implementation(projects.feature.note) // implementation(projects.feature.offline) diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt index 2f099eeb1ec..6b0f436c8a3 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt @@ -19,6 +19,7 @@ import com.mifos.feature.activate.di.ActivateModule import com.mifos.feature.auth.di.AuthModule import com.mifos.feature.center.di.CenterModule import com.mifos.feature.checker.inbox.task.di.CheckerInboxTaskModule +import com.mifos.feature.groups.di.GroupsModule import com.mifos.feature.individualCollectionSheet.di.CollectionSheetModule import com.mifos.feature.note.di.NoteModule import com.mifos.feature.pathTracking.di.PathTrackingModule @@ -63,7 +64,7 @@ object KoinModules { CollectionSheetModule, // DataTableModule, // DocumentModule, -// GroupsModule, + GroupsModule, // LoanModule, NoteModule, // OfflineModule, diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt index 74b78aa398b..e52a18ac9aa 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt @@ -18,11 +18,16 @@ import com.mifos.feature.about.navigation.aboutNavGraph import com.mifos.feature.activate.navigation.activateScreen import com.mifos.feature.activate.navigation.navigateToActivateScreen import com.mifos.feature.center.navigation.centerNavGraph +import com.mifos.feature.center.navigation.navigateCreateCenterScreenRoute import com.mifos.feature.checker.inbox.task.navigation.checkerInboxTaskNavGraph +import com.mifos.feature.groups.navigation.groupNavGraph +import com.mifos.feature.groups.navigation.navigateToCreateNewGroupScreen import com.mifos.feature.individualCollectionSheet.navigation.individualCollectionSheetNavGraph +import com.mifos.feature.note.navigation.navigateToNoteScreen import com.mifos.feature.note.navigation.noteNavGraph import com.mifos.feature.pathTracking.navigation.pathTrackingNavGraph import com.mifos.feature.savings.navigation.navigateToAddSavingsAccount +import com.mifos.feature.savings.navigation.navigateToSavingsAccountSummaryScreen import com.mifos.feature.savings.navigation.savingsNavGraph import com.mifos.feature.search.navigation.searchNavGraph import com.mifos.feature.settings.navigation.settingsScreen @@ -45,8 +50,8 @@ internal fun FeatureNavHost( searchNavGraph( paddingValues = padding, onCreateClient = { println("Create Client") }, - onCreateCenter = { println("Create Center") }, - onCreateGroup = { println("Create Group") }, + onCreateCenter = appState.navController::navigateCreateCenterScreenRoute, + onCreateGroup = appState.navController::navigateToCreateNewGroupScreen, onClient = { id -> println("Client clicked: $id") }, onCenter = { id -> println("Center clicked: $id") }, onGroup = { id -> println("Group clicked: $id") }, @@ -76,6 +81,20 @@ internal fun FeatureNavHost( }, ) + groupNavGraph( + navController = appState.navController, + paddingValues = padding, + addGroupLoanAccount = {}, + addSavingsAccount = appState.navController::navigateToAddSavingsAccount, + loadDocumentList = { _, _ -> }, + clientListFragment = {}, + loadSavingsAccountSummary = appState.navController::navigateToSavingsAccountSummaryScreen, + loadGroupDataTables = { _, _ -> }, + loadNotes = appState.navController::navigateToNoteScreen, + loadLoanAccountSummary = { _ -> }, + activateGroup = appState.navController::navigateToActivateScreen, + ) + settingsScreen( navigateBack = appState.navController::popBackStack, navigateToLoginScreen = {}, diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/HomeDestinationsScreen.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/HomeDestinationsScreen.kt index 44f0096f8ce..a1498af894e 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/HomeDestinationsScreen.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/HomeDestinationsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.feature.about.navigation.AboutScreens import com.mifos.feature.checker.inbox.task.navigation.CheckerInboxTaskScreens +import com.mifos.feature.groups.navigation.GroupScreen import com.mifos.feature.pathTracking.navigation.PathTrackingScreens import com.mifos.feature.search.navigation.SearchScreens import com.mifos.feature.settings.navigation.SettingsScreens @@ -42,7 +43,7 @@ sealed class HomeDestinationsScreen( data object GroupListScreen : HomeDestinationsScreen( title = "Groups", - route = "", + route = GroupScreen.GroupListScreen.route, icon = MifosIcons.Group, ) diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/CenterAccounts.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/CenterAccounts.kt index 678cb637a16..861a766a111 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/CenterAccounts.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/CenterAccounts.kt @@ -13,11 +13,13 @@ import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity +import kotlinx.serialization.Serializable /** * Created by mayankjindal on 11/07/17. */ @Parcelize +@Serializable data class CenterAccounts( val loanAccounts: List = emptyList(), diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/GroupAccounts.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/GroupAccounts.kt index 7453f6f430b..7f098b25a35 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/GroupAccounts.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/GroupAccounts.kt @@ -13,8 +13,10 @@ import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity +import kotlinx.serialization.Serializable @Parcelize +@Serializable data class GroupAccounts( var loanAccounts: List = emptyList(), diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt index c6173640d59..fd9516c3fc8 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt @@ -20,6 +20,7 @@ import com.mifos.room.utils.PrimaryKey import com.mifos.room.utils.UNDEFINED import com.mifos.room.utils.UNSPECIFIED import com.mifos.room.utils.VALUE_UNSPECIFIED +import kotlinx.serialization.Serializable @Entity( tableName = "LoanAccountEntity", @@ -47,6 +48,7 @@ import com.mifos.room.utils.VALUE_UNSPECIFIED ignoredColumns = [], ) @Parcelize +@Serializable data class LoanAccountEntity( @PrimaryKey(autoGenerate = true) val id: Int? = null, diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingAccountDepositTypeEntity.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingAccountDepositTypeEntity.kt index c01ca9dff79..e2d5de3f468 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingAccountDepositTypeEntity.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingAccountDepositTypeEntity.kt @@ -38,9 +38,9 @@ data class SavingAccountDepositTypeEntity( val isRecurring: Boolean get() = ServerTypes.RECURRING.id == id val endpoint: String - get() = ServerTypes.fromId(id!!).endpoint + get() = ServerTypes.fromId(id).endpoint val serverType: ServerTypes - get() = ServerTypes.fromId(id!!) + get() = ServerTypes.fromId(id) enum class ServerTypes(val id: Int, val code: String, val endpoint: String) { SAVINGS(100, "depositAccountType.savingsDeposit", APIEndPoint.SAVINGS_ACCOUNTS), @@ -49,7 +49,7 @@ data class SavingAccountDepositTypeEntity( ; companion object { - fun fromId(id: Int): ServerTypes { + fun fromId(id: Int?): ServerTypes { for (type in entries) { if (type.id == id) { return type diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingsAccountEntity.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingsAccountEntity.kt index 44dcc373fb6..d347cbb10b6 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingsAccountEntity.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/savings/SavingsAccountEntity.kt @@ -20,6 +20,7 @@ import com.mifos.room.utils.PrimaryKey import com.mifos.room.utils.UNDEFINED import com.mifos.room.utils.UNSPECIFIED import com.mifos.room.utils.VALUE_UNSPECIFIED +import kotlinx.serialization.Serializable @Entity( tableName = "SavingsAccount", @@ -55,6 +56,7 @@ import com.mifos.room.utils.VALUE_UNSPECIFIED ], ) @Parcelize +@Serializable data class SavingsAccountEntity( @PrimaryKey(autoGenerate = true) val id: Int? = null, diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/group/GroupPayloadEntity.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/group/GroupPayloadEntity.kt index 4673098a082..06635a32e61 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/group/GroupPayloadEntity.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/group/GroupPayloadEntity.kt @@ -13,6 +13,7 @@ import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize import com.mifos.room.utils.Entity import com.mifos.room.utils.PrimaryKey +import kotlinx.serialization.Serializable @Parcelize @Entity( @@ -23,6 +24,7 @@ import com.mifos.room.utils.PrimaryKey ignoredColumns = [], tableName = "GroupPayload", ) +@Serializable data class GroupPayloadEntity( @PrimaryKey(autoGenerate = true) val id: Int = 0, diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/zipmodels/GroupAndGroupAccounts.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/zipmodels/GroupAndGroupAccounts.kt index df265ae7b91..31852ca7291 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/zipmodels/GroupAndGroupAccounts.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/zipmodels/GroupAndGroupAccounts.kt @@ -13,11 +13,13 @@ import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize import com.mifos.room.entities.accounts.GroupAccounts import com.mifos.room.entities.group.GroupEntity +import kotlinx.serialization.Serializable /** * Created by Rajan Maurya on 11/09/16. */ @Parcelize +@Serializable data class GroupAndGroupAccounts( var group: GroupEntity? = null, var groupAccounts: GroupAccounts? = null, diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt index cf092ddbb2c..46afdda36eb 100644 --- a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt @@ -43,6 +43,7 @@ fun MifosTextFieldDropdown( .padding(horizontal = 16.dp), label: String? = null, readOnly: Boolean = false, + errorMessage: String? = null, ) { var isExpanded by remember { mutableStateOf(false) } @@ -51,6 +52,15 @@ fun MifosTextFieldDropdown( onExpandedChange = { isExpanded = !isExpanded }, ) { OutlinedTextField( + isError = errorMessage != null, + supportingText = { + errorMessage?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + }, value = value, onValueChange = onValueChanged, label = { label?.let { Text(it) } }, diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt index 1c3f1d4bc24..68c8cd5f5c7 100644 --- a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt @@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.ChevronLeft import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.FilterList @@ -59,7 +60,10 @@ import androidx.compose.material.icons.outlined.DateRange import androidx.compose.material.icons.outlined.EventRepeat import androidx.compose.material.icons.outlined.Group import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.HomeWork import androidx.compose.material.icons.outlined.Mail +import androidx.compose.material.icons.outlined.Numbers +import androidx.compose.material.icons.outlined.PersonOutline import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Wallet @@ -104,6 +108,7 @@ object MifosIcons { val EventRepeat = Icons.Outlined.EventRepeat val Date = Icons.Outlined.DateRange val ArrowBack1 = Icons.Rounded.ArrowBackIosNew + val DoneAll = Icons.Default.DoneAll val KeyboardArrowDown = Icons.Rounded.KeyboardArrowDown val Link = Icons.Default.Link val Server = Icons.Default @@ -179,6 +184,9 @@ object MifosIcons { val FlashOff = Icons.Default.FlashOff val Error2 = Icons.Filled.Error val Notifications = Icons.Filled.Notifications + val Numbers = Icons.Outlined.Numbers + val Homework = Icons.Outlined.HomeWork + val PersonOutline = Icons.Outlined.PersonOutline val NavigationDrawer = Icons.Default.Menu val Stop = Icons.Rounded.Stop diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt index d1f72e1402a..1ddac0dfbcb 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt @@ -46,12 +46,14 @@ import com.mifos.core.domain.useCases.GetClientPinpointLocationsUseCase import com.mifos.core.domain.useCases.GetClientSavingsAccountTemplateByProductUseCase import com.mifos.core.domain.useCases.GetDataTableInfoUseCase import com.mifos.core.domain.useCases.GetDocumentsListUseCase +import com.mifos.core.domain.useCases.GetGroupDetailsUseCase import com.mifos.core.domain.useCases.GetGroupLoansAccountTemplateUseCase import com.mifos.core.domain.useCases.GetGroupSavingsAccountTemplateByProductUseCase import com.mifos.core.domain.useCases.GetGroupsByCenterUseCase import com.mifos.core.domain.useCases.GetGroupsByOfficeUseCase import com.mifos.core.domain.useCases.GetIndividualCollectionSheetUseCase import com.mifos.core.domain.useCases.GetListOfLoanChargesUseCase +import com.mifos.core.domain.useCases.GetLoanAndLoanRepaymentUseCase import com.mifos.core.domain.useCases.GetLoansAccountTemplateUseCase import com.mifos.core.domain.useCases.GetReportCategoryUseCase import com.mifos.core.domain.useCases.GetReportFullParameterListUseCase @@ -59,6 +61,7 @@ import com.mifos.core.domain.useCases.GetReportParameterDetailsUseCase import com.mifos.core.domain.useCases.GetRunReportOfficesUseCase import com.mifos.core.domain.useCases.GetRunReportProductUseCase import com.mifos.core.domain.useCases.GetRunReportWithQueryUseCase +import com.mifos.core.domain.useCases.GetSavingsAccountAndTemplateUseCase import com.mifos.core.domain.useCases.GetStaffInOfficeUseCase import com.mifos.core.domain.useCases.GetUserPathTrackingUseCase import com.mifos.core.domain.useCases.GroupsListPagingDataSource @@ -153,4 +156,7 @@ val UseCaseModule = module { factoryOf(::ValidateServerEndPointUseCase) factoryOf(::ValidateServerPortUseCase) factoryOf(::ValidateServerTenantUseCase) + factoryOf(::GetGroupDetailsUseCase) + factoryOf(::GetLoanAndLoanRepaymentUseCase) + factoryOf(::GetSavingsAccountAndTemplateUseCase) } diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt new file mode 100644 index 00000000000..41cf111e059 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repository.GroupDetailsRepository +import com.mifos.room.entities.zipmodels.GroupAndGroupAccounts +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class GetGroupDetailsUseCase( + private val repository: GroupDetailsRepository, +) { + operator fun invoke(groupId: Int): Flow> = + combine( + repository.getGroup(groupId), + repository.getGroupAccounts(groupId), + ) { group, groupAccounts -> + if (group is DataState.Success && groupAccounts is DataState.Success) { + DataState.Success( + GroupAndGroupAccounts( + group = group.data, + groupAccounts = groupAccounts.data, + ), + ) + } else if (group is DataState.Error || groupAccounts is DataState.Error) { + val exception = (group as? DataState.Error)?.exception + ?: (groupAccounts as? DataState.Error)?.exception + ?: Exception("Unknown error") + DataState.Error(exception) + } else { + DataState.Loading + } + } +} diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetLoanAndLoanRepaymentUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetLoanAndLoanRepaymentUseCase.kt new file mode 100644 index 00000000000..90106b9da9e --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetLoanAndLoanRepaymentUseCase.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repository.SyncGroupsDialogRepository +import com.mifos.room.entities.zipmodels.LoanAndLoanRepayment +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class GetLoanAndLoanRepaymentUseCase( + private val repository: SyncGroupsDialogRepository, +) { + operator fun invoke(loanId: Int): Flow> = + combine( + repository.syncLoanById(loanId), + repository.syncLoanRepaymentTemplate(loanId), + ) { loan, template -> + if (loan is DataState.Success && template is DataState.Success) { + DataState.Success( + LoanAndLoanRepayment( + loanWithAssociations = loan.data, + loanRepaymentTemplate = template.data, + ), + ) + } else if (loan is DataState.Error || template is DataState.Error) { + val exception = (loan as? DataState.Error)?.exception + ?: (template as? DataState.Error)?.exception + ?: Exception("Unknown error") + DataState.Error(exception) + } else { + DataState.Loading + } + } +} diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsAccountAndTemplateUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsAccountAndTemplateUseCase.kt new file mode 100644 index 00000000000..84e167bb4c0 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsAccountAndTemplateUseCase.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.domain.useCases + +import com.mifos.core.common.utils.Constants +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repository.SyncGroupsDialogRepository +import com.mifos.room.entities.zipmodels.SavingsAccountAndTransactionTemplate +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class GetSavingsAccountAndTemplateUseCase( + private val repository: SyncGroupsDialogRepository, +) { + operator fun invoke( + savingsAccountType: String, + savingsAccountId: Int, + ): Flow> = + combine( + repository.syncSavingsAccount( + savingsAccountType, + savingsAccountId, + Constants.TRANSACTIONS, + ), + repository.syncSavingsAccountTransactionTemplate( + savingsAccountType, + savingsAccountId, + Constants.SAVINGS_ACCOUNT_TRANSACTION_DEPOSIT, + ), + ) { savings, template -> + if (savings is DataState.Success && template is DataState.Success) { + DataState.Success( + SavingsAccountAndTransactionTemplate( + savingsAccountWithAssociations = savings.data, + savingsAccountTransactionTemplate = template.data, + ), + ) + } else if (savings is DataState.Error || template is DataState.Error) { + val exception = (savings as? DataState.Error)?.exception + ?: (template as? DataState.Error)?.exception + ?: Exception("Unknown error") + DataState.Error(exception) + } else { + DataState.Loading + } + } +} diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/Note.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/Note.kt index ccba86500d8..3eccea7b901 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/Note.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/Note.kt @@ -11,14 +11,16 @@ package com.mifos.core.model.objects import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize +import kotlinx.serialization.Serializable @Parcelize +@Serializable data class Note( val id: Int? = null, val clientId: Int? = null, - val noteContent: String? = null, + val note: String? = null, val createdById: Int? = null, diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerOffices.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerOffices.kt index c20ecc4d3ed..debf2e14d91 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerOffices.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerOffices.kt @@ -36,11 +36,7 @@ class DataManagerOffices( * return all List of Offices from DatabaseHelperOffices */ fun fetchOffices(): Flow> { - return baseApiManager.getOfficeApi() - .retrieveOffices(null, null, null) - .map { responseList -> - responseList.map(GetOfficeResponseMapper::mapFromEntity) - } + return mBaseApiManager.officeApi.allOffices() } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetOfficesResponse.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetOfficesResponse.kt index a5a97779aab..b270ea1587b 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetOfficesResponse.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetOfficesResponse.kt @@ -9,8 +9,6 @@ */ package com.mifos.core.network.model -import kotlinx.datetime.LocalDate -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable /** @@ -46,7 +44,5 @@ data class GetOfficesResponse( val nameDecorated: String? = null, - @Contextual - val openingDate: LocalDate? = null, - + val openingDate: List? = null, ) diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosAlertDialog.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosAlertDialog.kt index a507020500b..161cb080092 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosAlertDialog.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosAlertDialog.kt @@ -59,7 +59,7 @@ fun MifosAlertDialog( @DevicePreview @Composable -fun MifosAlertDialogPreview( +private fun MifosAlertDialogPreview( modifier: Modifier = Modifier, ) { MifosTheme { diff --git a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterViewModel.kt b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterViewModel.kt index 1f64181f17b..8500c9bd5a2 100644 --- a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterViewModel.kt +++ b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterViewModel.kt @@ -20,7 +20,6 @@ import com.mifos.core.data.repository.NewIndividualCollectionSheetRepository import com.mifos.room.entities.center.CenterPayloadEntity import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch class CreateNewCenterViewModel( @@ -34,14 +33,8 @@ class CreateNewCenterViewModel( fun loadOffices() { viewModelScope.launch { - _createNewCenterUiState.value = - CreateNewCenterUiState.Loading - collectionSheetRepo.offices() - .catch { - _createNewCenterUiState.value = - CreateNewCenterUiState.Error(Res.string.feature_center_failed_to_load_offices) - }.collect { + .collect { when (it) { is DataState.Error -> { _createNewCenterUiState.value = diff --git a/feature/groups/build.gradle.kts b/feature/groups/build.gradle.kts index 5dede6b88b5..688bfe39604 100644 --- a/feature/groups/build.gradle.kts +++ b/feature/groups/build.gradle.kts @@ -8,31 +8,36 @@ * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifos.android.feature) - alias(libs.plugins.mifos.android.library.compose) - alias(libs.plugins.mifos.android.library.jacoco) + alias(libs.plugins.mifos.cmp.feature) + alias(libs.plugins.mifos.kmp.koin) } - android { namespace = "com.mifos.feature.groups" } -dependencies { - implementation(projects.core.domain) - - // swipe refresh - implementation(libs.accompanist.swiperefresh) - - // paging 3 - implementation(libs.androidx.paging.runtime.ktx) - implementation(libs.androidx.paging.compose) - - //DBFlow dependencies - implementation(libs.dbflow) - - androidTestImplementation(libs.androidx.compose.ui.test) - debugApi(libs.androidx.compose.ui.test.manifest) - - testImplementation(libs.hilt.android.testing) -} \ No newline at end of file +kotlin{ + sourceSets{ + commonMain.dependencies { + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.ui) + + implementation(projects.core.model) + implementation(projects.core.domain) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.designsystem) + + implementation(compose.components.uiToolingPreview) + implementation(libs.androidx.paging.common) + } + + androidMain.dependencies { + implementation(libs.androidx.paging.compose) + implementation(compose.ui) + implementation(compose.material3) + implementation(libs.androidx.compose.foundation) + } + } +} diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt b/feature/groups/src/androidMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.android.kt similarity index 70% rename from feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt rename to feature/groups/src/androidMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.android.kt index 96c028313fa..b1da6adc93d 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt +++ b/feature/groups/src/androidMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.android.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -9,37 +9,34 @@ */ package com.mifos.feature.groups.groupList +import androidclient.feature.groups.generated.resources.Res +import androidclient.feature.groups.generated.resources.feature_groups_failed_to_fetch_groups +import androidclient.feature.groups.generated.resources.feature_groups_no_more_groups_available +import androidclient.feature.groups.generated.resources.feature_groups_sync import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.DoneAll -import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -47,13 +44,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.DarkGray -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle @@ -63,9 +55,6 @@ import androidx.compose.ui.unit.sp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.SwipeRefreshState -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosPaginationSweetError import com.mifos.core.designsystem.component.MifosPagingAppendProgress @@ -74,25 +63,20 @@ import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.ui.components.MifosEmptyUi import com.mifos.core.ui.components.MifosFAB import com.mifos.core.ui.components.SelectionModeTopAppBar -import com.mifos.feature.groups.R import com.mifos.feature.groups.syncGroupDialog.SyncGroupDialogScreen import com.mifos.room.entities.group.GroupEntity -import org.koin.androidx.compose.koinViewModel +import org.jetbrains.compose.resources.stringResource @Composable -internal fun GroupsListRoute( +internal actual fun GroupsListRoute( paddingValues: PaddingValues, onAddGroupClick: () -> Unit, - onGroupClick: (groupId: Int) -> Unit, - viewModel: GroupsListViewModel = koinViewModel(), + onGroupClick: (Int) -> Unit, + viewModel: GroupsListViewModel, ) { val data = viewModel.data.collectAsLazyPagingItems() val lazyListState = rememberLazyListState() - val swipeRefreshState = rememberSwipeRefreshState( - isRefreshing = data.loadState.refresh is LoadState.Loading, - ) - val selectedItems = remember { mutableStateListOf() } @@ -108,7 +92,6 @@ internal fun GroupsListRoute( .fillMaxSize() .padding(paddingValues), lazyListState = lazyListState, - swipeRefreshState = swipeRefreshState, selectedItems = selectedItems, data = data, onAddGroupClick = onAddGroupClick, @@ -126,18 +109,22 @@ internal fun GroupsListRoute( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupsListScreen( lazyListState: LazyListState, - swipeRefreshState: SwipeRefreshState, selectedItems: List, data: LazyPagingItems, onAddGroupClick: () -> Unit, onGroupClick: (groupId: Int) -> Unit, onSelectItem: (GroupEntity) -> Unit, modifier: Modifier = Modifier, + // todo isrefreshing logic needs to be implemented + isRefreshing: Boolean = false, resetSelectionMode: () -> Unit, ) { + val pullRefreshState = rememberPullToRefreshState() + var syncGroups by rememberSaveable { mutableStateOf(false) } if (syncGroups) { SyncGroupDialogScreen( @@ -178,18 +165,19 @@ fun GroupsListScreen( imageVector = MifosIcons.Sync, contentDescription = "Sync Items", ) - Text(text = stringResource(id = R.string.feature_groups_sync)) + Text(text = stringResource(Res.string.feature_groups_sync)) } }, ) } }, ) { paddingValues -> - SwipeRefresh( + PullToRefreshBox( modifier = Modifier.semantics { contentDescription = "SwipeRefresh::GroupList" }, - state = swipeRefreshState, + state = pullRefreshState, + isRefreshing = isRefreshing, onRefresh = { data.refresh() }, ) { LazyColumn( @@ -220,7 +208,7 @@ private fun LazyListScope.refreshState(data: LazyPagingItems) { is LoadState.Error -> { item { MifosSweetError( - message = stringResource(id = R.string.feature_groups_failed_to_fetch_groups), + message = stringResource(Res.string.feature_groups_failed_to_fetch_groups), onclick = { data.refresh() }, ) } @@ -236,7 +224,7 @@ private fun LazyListScope.refreshState(data: LazyPagingItems) { if (data.itemCount < 1) { item { MifosEmptyUi( - text = stringResource(id = R.string.feature_groups_no_more_groups_available), + text = stringResource(Res.string.feature_groups_no_more_groups_available), ) } } @@ -267,7 +255,7 @@ private fun LazyListScope.appendState(data: LazyPagingItems) { modifier = Modifier .fillMaxWidth() .padding(6.dp), - text = stringResource(id = R.string.feature_groups_no_more_groups_available), + text = stringResource(Res.string.feature_groups_no_more_groups_available), style = TextStyle(fontSize = 14.sp), color = DarkGray, textAlign = TextAlign.Center, @@ -303,64 +291,3 @@ private fun LazyListScope.successState( } } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun GroupItem( - group: GroupEntity, - doesSelected: Boolean, - inSelectionMode: Boolean, - onGroupClick: () -> Unit, - modifier: Modifier = Modifier, - onSelectItem: () -> Unit, -) { - val borderStroke = if (doesSelected) { - BorderStroke(1.dp, Color.Blue) - } else { - CardDefaults.outlinedCardBorder() - } - val containerColor = if (doesSelected) Color.Blue else Color.Unspecified - - group.name?.let { - OutlinedCard( - modifier = modifier - .testTag(it) - .fillMaxWidth() - .padding(8.dp) - .height(70.dp) - .clip(RoundedCornerShape(8.dp)) - .combinedClickable( - onClick = { - if (inSelectionMode) { - onSelectItem() - } else { - onGroupClick() - } - }, - onLongClick = onSelectItem, - ), - shape = RoundedCornerShape(8.dp), - colors = CardDefaults.outlinedCardColors( - containerColor = containerColor, - ), - border = borderStroke, - ) { - Row( - modifier = Modifier - .fillMaxSize() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = it, - style = MaterialTheme.typography.titleMedium, - ) - - if (group.sync) { - Icon(imageVector = Icons.Default.DoneAll, contentDescription = "Sync") - } - } - } - } -} diff --git a/feature/groups/src/androidTest/java/com/mifos/feature/groups/GroupEntityListScreenTest.kt b/feature/groups/src/androidTest/java/com/mifos/feature/groups/GroupEntityListScreenTest.kt deleted file mode 100644 index aed96da2e2e..00000000000 --- a/feature/groups/src/androidTest/java/com/mifos/feature/groups/GroupEntityListScreenTest.kt +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.groups - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.test.assertContentDescriptionContains -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotDisplayed -import androidx.compose.ui.test.assertTextContains -import androidx.compose.ui.test.captureToImage -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.longClick -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.onLast -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTouchInput -import androidx.paging.CombinedLoadStates -import androidx.paging.LoadState -import androidx.paging.LoadStates -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingSource -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import com.google.accompanist.swiperefresh.SwipeRefreshState -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import com.mifos.core.designsystem.theme.BlueSecondary -import com.mifos.core.domain.useCases.GroupsListPagingDataSource -import com.mifos.core.entity.group.Group -import com.mifos.core.testing.repository.TestGroupsListRepository -import com.mifos.core.testing.repository.sampleGroups -import com.mifos.feature.groups.groupList.GroupsListScreen -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.runBlocking -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -class GroupEntityListScreenTest { - - @get:Rule - val composeTestRule = createAndroidComposeRule() - - private val repository = TestGroupsListRepository() - private val pageSize = 10 - - private fun createPager( - config: PagingConfig = PagingConfig( - pageSize = pageSize, - enablePlaceholders = true, - initialLoadSize = pageSize, - prefetchDistance = 0, - ), - pagingSourceFactory: () -> PagingSource = { - GroupsListPagingDataSource(repository, pageSize) - }, - ): Pager { - return Pager(config = config, pagingSourceFactory = pagingSourceFactory) - } - - private fun createPagerWithoutPlaceholder( - config: PagingConfig = PagingConfig( - pageSize = pageSize, - enablePlaceholders = false, - initialLoadSize = pageSize, - prefetchDistance = 1, - ), - pagingSourceFactory: () -> PagingSource = { - GroupsListPagingDataSource(repository, pageSize) - }, - ): Pager { - return Pager(config = config, pagingSourceFactory = pagingSourceFactory) - } - - @Test - fun checkForInitialLoadingState() { - val pager = createPager() - val loadStates: MutableList = mutableListOf() - composeTestRule.setContent { - val lazyPagingItems = pager.flow.collectAsLazyPagingItems() - loadStates.add(lazyPagingItems.loadState) - } - - composeTestRule.waitForIdle() - - val expected = CombinedLoadStates( - refresh = LoadState.Loading, - prepend = LoadState.NotLoading(false), - append = LoadState.NotLoading(false), - source = LoadStates( - LoadState.Loading, - LoadState.NotLoading(false), - LoadState.NotLoading(false), - ), - mediator = null, - ) - assert(loadStates.isNotEmpty()) - assertEquals(loadStates.first(), expected) - } - - @Test - fun checkForEmptyDataState() { - val pager = createPager() - - repository.setGroupsData(emptyList()) - lateinit var lazyPagingItems: LazyPagingItems - - composeTestRule.setContent { - lazyPagingItems = pager.flow.collectAsLazyPagingItems() - - GroupsListScreen( - lazyListState = rememberLazyListState(), - swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), - data = lazyPagingItems, - selectedItems = emptyList(), - onGroupClick = {}, - onSelectItem = {}, - onAddGroupClick = {}, - resetSelectionMode = {}, - ) - } - - composeTestRule.waitForIdle() - - assertEquals(LoadState.NotLoading(false), lazyPagingItems.loadState.refresh) - - composeTestRule - .onNodeWithContentDescription("MifosEmptyUi") - .assertExists() - .onChildren() - .onLast() - .assertTextContains(composeTestRule.activity.resources.getString(R.string.feature_groups_no_more_groups_available)) - } - - @Test - fun checkForRefreshState() { - val pager = createPager() - - lateinit var lazyPagingItems: LazyPagingItems - lateinit var refreshState: SwipeRefreshState - - composeTestRule.setContent { - // Adding some delay to test refresh state - lazyPagingItems = pager.flow.onStart { delay(5000) }.collectAsLazyPagingItems() - refreshState = rememberSwipeRefreshState( - isRefreshing = lazyPagingItems.loadState.refresh is LoadState.Loading, - ) - - GroupsListScreen( - lazyListState = rememberLazyListState(), - swipeRefreshState = refreshState, - data = lazyPagingItems, - selectedItems = emptyList(), - onGroupClick = {}, - onSelectItem = {}, - onAddGroupClick = {}, - resetSelectionMode = {}, - ) - } - - // Check SwipeRefresh is displayed in screen - composeTestRule - .onNodeWithContentDescription("SwipeRefresh::GroupList") - .assertIsDisplayed() - - // Check initial refresh state is true - assertEquals(true, refreshState.isRefreshing) - - // Perform refresh and check refresh state is true - lazyPagingItems.refresh() - - assertEquals(true, refreshState.isRefreshing) - } - - @Test - fun checkForErrorStateAndRetryFunction() { - val pager = createPager() - - repository.setGroupsData(null) - lateinit var lazyPagingItems: LazyPagingItems - - composeTestRule.setContent { - lazyPagingItems = pager.flow.collectAsLazyPagingItems() - - GroupsListScreen( - lazyListState = rememberLazyListState(), - swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), - data = lazyPagingItems, - selectedItems = emptyList(), - onGroupClick = {}, - onSelectItem = {}, - onAddGroupClick = {}, - resetSelectionMode = {}, - ) - } - - composeTestRule.waitForIdle() - - composeTestRule - .onNodeWithContentDescription("MifosSweetError") - .assertIsDisplayed() - .onChildren()[1] - .assertTextContains(composeTestRule.activity.resources.getString(R.string.feature_groups_failed_to_fetch_groups)) - - composeTestRule - .onNodeWithContentDescription("MifosSweetError") - .assertIsDisplayed() - .onChildren()[2] - .assertHasClickAction() - .assertTextContains("Try Again") - - repository.setGroupsData(sampleGroups) - - composeTestRule - .onNodeWithContentDescription("MifosSweetError") - .assertIsDisplayed() - .onChildren()[2] - .assertHasClickAction() - .assertTextContains("Try Again") - .performClick() - } - - @Test - fun checkForSuccessfulState() { - repository.setGroupsData(sampleGroups) - val pager = createPager() - - lateinit var lazyPagingItems: LazyPagingItems - lateinit var lazyListState: LazyListState - - composeTestRule.setContent { - lazyListState = rememberLazyListState() - lazyPagingItems = pager.flow.collectAsLazyPagingItems() - - GroupsListScreen( - lazyListState = lazyListState, - swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), - data = lazyPagingItems, - selectedItems = emptyList(), - onGroupClick = {}, - onSelectItem = {}, - onAddGroupClick = {}, - resetSelectionMode = {}, - ) - } - - assertEquals(LoadState.NotLoading(false), lazyPagingItems.loadState.refresh) - - composeTestRule.waitForIdle() - - composeTestRule - .onNodeWithContentDescription("MifosSweetError") - .assertIsNotDisplayed() - - composeTestRule.waitForIdle() - - assertEquals( - expected = sampleGroups.take(pageSize), - actual = lazyPagingItems.itemSnapshotList.items, - message = "Sample Groups are differ from Snapshot items", - ) - - // Check paged item is being displayed - sampleGroups.take(pageSize).forEach { group -> - group.name?.let { - composeTestRule - .onNodeWithTag(it) - .assertIsDisplayed() - } - } - - composeTestRule.waitForIdle() - - // And not more than item will displayed - sampleGroups[pageSize + 1].name?.let { - composeTestRule - .onNodeWithTag(it) - .assertDoesNotExist() - } - } - - @Test - fun checkForAppendDataState() { - val pager = createPagerWithoutPlaceholder() - - lateinit var lazyPagingItems: LazyPagingItems - lateinit var lazyListState: LazyListState - - composeTestRule.setContent { - lazyListState = rememberLazyListState() - lazyPagingItems = pager.flow.collectAsLazyPagingItems() - - GroupsListScreen( - lazyListState = lazyListState, - swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), - data = lazyPagingItems, - selectedItems = emptyList(), - onGroupClick = {}, - onSelectItem = {}, - onAddGroupClick = {}, - resetSelectionMode = {}, - ) - } - - composeTestRule.waitForIdle() - - composeTestRule.runOnIdle { - runBlocking { - lazyListState.scrollToItem(pageSize + 1) - } - } - - assertEquals( - pageSize * 2, - lazyPagingItems.itemSnapshotList.items.size, - ) - - assertEquals( - LoadState.NotLoading(false), - lazyPagingItems.loadState.append, - ) - - composeTestRule.runOnIdle { - runBlocking { - lazyListState.scrollToItem(pageSize * 2 + 1) - } - } - - composeTestRule.waitForIdle() - - // end of pagination reached so total item will be (pageSize * 3) + 1 = 31 - assertEquals( - pageSize * 3 + 1, - lazyListState.layoutInfo.totalItemsCount, - ) - } - - @Test - fun checkTheSelectedGroupItemAndVisibilityOfSelectionModeTopAppBar() { - val pager = createPager() - val selectedItems = mutableStateListOf() - - lateinit var lazyPagingItems: LazyPagingItems - lateinit var lazyListState: LazyListState - var surfaceColor = Color.Unspecified - composeTestRule.mainClock.autoAdvance = false - - composeTestRule.setContent { - lazyListState = rememberLazyListState() - lazyPagingItems = pager.flow.collectAsLazyPagingItems() - surfaceColor = MaterialTheme.colorScheme.surface - - GroupsListScreen( - lazyListState = lazyListState, - swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), - data = lazyPagingItems, - selectedItems = selectedItems.toList(), - onGroupClick = {}, - onSelectItem = { - if (selectedItems.contains(it)) { - selectedItems.remove(it) - } else { - selectedItems.add(it) - } - }, - onAddGroupClick = {}, - resetSelectionMode = { - selectedItems.clear() - }, - ) - } - - assertEquals(LoadState.NotLoading(false), lazyPagingItems.loadState.refresh) - - composeTestRule.waitForIdle() - - // Check Group is displayed in screen and has click action - sampleGroups[2].name?.let { - composeTestRule - .onNodeWithTag(it) - .assertIsDisplayed() - .assertHasClickAction() - } - - // Perform long click that item - sampleGroups[2].name?.let { - composeTestRule - .onNodeWithTag(it) - .assertIsDisplayed() - .assertHasClickAction() - .performTouchInput { - longClick() - } - } - - composeTestRule.mainClock.advanceTimeByFrame() - composeTestRule.waitForIdle() - composeTestRule.mainClock.advanceTimeByFrame() - composeTestRule.mainClock.advanceTimeBy(250) - - // Check item has been selected - assert(selectedItems.contains(sampleGroups[2])) - - // Select another group item - sampleGroups[4].name?.let { - composeTestRule - .onNodeWithTag(it) - .assertIsDisplayed() - .assertHasClickAction() - .performTouchInput { - longClick() - } - } - - // Check item has been selected - assert(selectedItems.contains(sampleGroups[4])) - - // Check both item has been selected - assertEquals( - 2, - selectedItems.size, - ) - - composeTestRule.waitForIdle() - - // check for background color change of selected item - sampleGroups[4].name?.let { - val data = composeTestRule - .onNodeWithTag(it) - .captureToImage() - - assertEquals(BlueSecondary.colorSpace, data.colorSpace) - } - - composeTestRule.waitUntil { - selectedItems.size == 2 - } - - composeTestRule.waitForIdle() - - // check for Contextual TopAppBar Visibility - composeTestRule - .onNodeWithContentDescription("GroupList::ContextualTopAppBar") - .assertIsDisplayed() - - // Check reset selection IconButton is visible or not - composeTestRule - .onNodeWithContentDescription("reset selection") - .assertIsDisplayed() - .assertHasClickAction() - - // Check selected text is visible or not - composeTestRule - .onNodeWithContentDescription("GroupList::ContextualTopAppBar") - .assertIsDisplayed() - .onChildren()[1] - .assertTextContains("${selectedItems.size} selected") - .assertIsDisplayed() - - // Check Sync Button is visible or not - composeTestRule - .onNodeWithContentDescription("GroupList::ContextualTopAppBar") - .assertIsDisplayed() - .onChildren()[2] - .assertContentDescriptionContains("Sync Items") - .assertIsDisplayed() - .assertHasClickAction() - - // now deselect an item by clicking and check item has been removed or not from list - sampleGroups[2].name?.let { - composeTestRule - .onNodeWithTag(it) - .performClick() - } - - // check item removed from selected list or not - assert(!selectedItems.contains(sampleGroups[2])) - - // and selected list only contain 1 item - assert(selectedItems.size == 1) - - // also check the background color of that item is set to surfaceColor - sampleGroups[2].name?.let { - val data = composeTestRule - .onNodeWithTag(it) - .captureToImage() - - assertEquals(data.colorSpace, surfaceColor.colorSpace) - } - - // Deselect all item by clicking on reset-selection button - composeTestRule - .onNodeWithContentDescription("reset selection") - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - - composeTestRule.waitForIdle() - - // check selected items list should be empty - assert(selectedItems.isEmpty()) - } -} diff --git a/feature/groups/src/main/res/values/strings.xml b/feature/groups/src/commonMain/composeResources/values/strings.xml similarity index 100% rename from feature/groups/src/main/res/values/strings.xml rename to feature/groups/src/commonMain/composeResources/values/strings.xml diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt similarity index 68% rename from feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt index 19a0de6040d..3c59a51c7b3 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt @@ -9,8 +9,21 @@ */ package com.mifos.feature.groups.createNewGroup -import android.content.Context -import android.widget.Toast +import androidclient.feature.groups.generated.resources.Res +import androidclient.feature.groups.generated.resources.feature_groups_activation_date +import androidclient.feature.groups.generated.resources.feature_groups_active +import androidclient.feature.groups.generated.resources.feature_groups_create_new_group +import androidclient.feature.groups.generated.resources.feature_groups_dismiss +import androidclient.feature.groups.generated.resources.feature_groups_error_group_name_cannot_be_empty +import androidclient.feature.groups.generated.resources.feature_groups_error_group_name_must_be_at_least_four_characters_long +import androidclient.feature.groups.generated.resources.feature_groups_error_group_name_should_contain_only_alphabets +import androidclient.feature.groups.generated.resources.feature_groups_error_office_not_selected +import androidclient.feature.groups.generated.resources.feature_groups_external_id +import androidclient.feature.groups.generated.resources.feature_groups_name +import androidclient.feature.groups.generated.resources.feature_groups_office_name_mandatory +import androidclient.feature.groups.generated.resources.feature_groups_select_date +import androidclient.feature.groups.generated.resources.feature_groups_submit +import androidclient.feature.groups.generated.resources.feature_groups_submit_date import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -50,14 +63,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -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.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.common.utils.DateHelper +import com.mifos.core.common.utils.formatDate import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosDatePickerTextField import com.mifos.core.designsystem.component.MifosOutlinedTextField @@ -65,12 +75,16 @@ import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown import com.mifos.core.model.objects.responses.SaveResponse -import com.mifos.feature.groups.R +import com.mifos.core.ui.components.MifosAlertDialog import com.mifos.room.entities.group.GroupPayloadEntity import com.mifos.room.entities.organisation.OfficeEntity -import org.koin.androidx.compose.koinViewModel -import java.text.SimpleDateFormat -import java.util.Locale +import kotlinx.datetime.Clock +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.PreviewParameter +import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider +import org.koin.compose.viewmodel.koinViewModel /** * Created by Pronay Sarker on 30/06/2024 (7:53 AM) @@ -99,6 +113,7 @@ internal fun CreateNewGroupScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CreateNewGroupScreen( uiState: CreateNewGroupUiState, @@ -108,19 +123,16 @@ internal fun CreateNewGroupScreen( modifier: Modifier = Modifier, getResponse: () -> String, ) { - val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } MifosScaffold( modifier = modifier, - title = stringResource(id = R.string.feature_groups_create_new_group), + title = stringResource(Res.string.feature_groups_create_new_group), onBackPressed = {}, snackbarHostState = snackbarHostState, ) { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), + modifier = Modifier.padding(paddingValues).fillMaxSize(), ) { when (uiState) { is CreateNewGroupUiState.ShowFetchingError -> { @@ -131,9 +143,15 @@ internal fun CreateNewGroupScreen( } is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { - Toast.makeText(context, "Group " + getResponse(), Toast.LENGTH_LONG) - .show() - onGroupCreated.invoke(uiState.saveResponse) + MifosAlertDialog( + dialogTitle = "Success", + dialogText = "Group" + getResponse(), + confirmationText = "OK", + onConfirmation = { + onGroupCreated.invoke(uiState.saveResponse) + }, + onDismissRequest = { }, + ) } is CreateNewGroupUiState.ShowOffices -> { @@ -177,26 +195,38 @@ private fun CreateNewGroupContent( mutableStateOf(false) } - val context = LocalContext.current + var groupValidationError: StringResource? by rememberSaveable { mutableStateOf(null) } + var officeValidationError: StringResource? by rememberSaveable { mutableStateOf(null) } + val density = LocalDensity.current val scrollState = rememberScrollState() var officeId by rememberSaveable { mutableIntStateOf(0) } - var activationDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } - var submittedOnDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + + var activationDate by rememberSaveable { + mutableLongStateOf( + Clock.System.now().toEpochMilliseconds(), + ) + } + var submittedOnDate by rememberSaveable { + mutableLongStateOf( + Clock.System.now().toEpochMilliseconds(), + ) + } val activateDatePickerState = rememberDatePickerState( initialSelectedDateMillis = activationDate, selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { - return utcTimeMillis >= System.currentTimeMillis() + return utcTimeMillis >= Clock.System.now().toEpochMilliseconds() } }, ) + val sumittedDatePickerState = rememberDatePickerState( initialSelectedDateMillis = submittedOnDate, selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { - return utcTimeMillis >= System.currentTimeMillis() + return utcTimeMillis >= Clock.System.now().toEpochMilliseconds() } }, ) @@ -222,7 +252,7 @@ private fun CreateNewGroupContent( submitDatePicker = false activationDatePicker = false }, - ) { Text(stringResource(id = R.string.feature_groups_select_date)) } + ) { Text(stringResource(Res.string.feature_groups_select_date)) } }, dismissButton = { TextButton( @@ -230,7 +260,7 @@ private fun CreateNewGroupContent( activationDatePicker = false submitDatePicker = false }, - ) { Text(stringResource(id = R.string.feature_groups_dismiss)) } + ) { Text(stringResource(Res.string.feature_groups_dismiss)) } }, ) { DatePicker(state = if (submitDatePicker) sumittedDatePickerState else activateDatePickerState) @@ -246,9 +276,12 @@ private fun CreateNewGroupContent( MifosOutlinedTextField( value = groupName, - onValueChange = { groupName = it }, - label = stringResource(id = R.string.feature_groups_name), - error = null, + onValueChange = { + groupName = it + groupValidationError = null + }, + label = stringResource(Res.string.feature_groups_name), + error = groupValidationError?.let { stringResource(it) }, ) Spacer(modifier = Modifier.height(16.dp)) @@ -263,19 +296,19 @@ private fun CreateNewGroupContent( officeList[index].id.let { officeId = it } + officeValidationError = null }, - label = R.string.feature_groups_office_name_mandatory, + label = stringResource(Res.string.feature_groups_office_name_mandatory), options = officeList.map { it.name.toString() }, readOnly = true, + errorMessage = officeValidationError?.let { stringResource(it) }, ) Spacer(modifier = Modifier.height(16.dp)) MifosDatePickerTextField( - value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( - submittedOnDate, - ), - label = stringResource(R.string.feature_groups_submit_date), + value = DateHelper.getDateAsStringFromLong(submittedOnDate), + label = stringResource(Res.string.feature_groups_submit_date), openDatePicker = { submitDatePicker = true }, @@ -286,7 +319,7 @@ private fun CreateNewGroupContent( MifosOutlinedTextField( value = externalId, onValueChange = { externalId = it }, - label = stringResource(id = R.string.feature_groups_external_id), + label = stringResource(Res.string.feature_groups_external_id), error = null, ) @@ -297,13 +330,10 @@ private fun CreateNewGroupContent( ) { Checkbox( modifier = Modifier.padding(start = 8.dp), -// colors = CheckboxDefaults.colors( -// if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, -// ), checked = isActive, onCheckedChange = { isActive = !isActive }, ) - Text(text = stringResource(id = R.string.feature_groups_active)) + Text(text = stringResource(Res.string.feature_groups_active)) } AnimatedVisibility( @@ -320,10 +350,8 @@ private fun CreateNewGroupContent( Spacer(modifier = Modifier.height(16.dp)) MifosDatePickerTextField( - value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( - activationDate, - ), - label = stringResource(R.string.feature_groups_activation_date), + value = DateHelper.getDateAsStringFromLong(activationDate), + label = stringResource(Res.string.feature_groups_activation_date), openDatePicker = { activationDatePicker = true }, @@ -337,29 +365,13 @@ private fun CreateNewGroupContent( .fillMaxWidth() .padding(horizontal = 16.dp) .heightIn(46.dp), -// colors = ButtonDefaults.buttonColors( -// containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, -// ), onClick = { - if (validateFields(groupName, selectedOffice, context)) { -// if (Network.isOnline(context)) { - val activationDateInString = if (isActive) { - SimpleDateFormat( - "dd MMMM yyyy", - Locale.getDefault(), - ).format( - activationDate, - ) - } else { - null - } - - val submittedOnDateInString = SimpleDateFormat( - "dd MMMM yyyy", - Locale.getDefault(), - ).format( - submittedOnDate, - ) + groupValidationError = validateGroupField(groupName) + officeValidationError = validateOffice(selectedOffice) + + if (groupValidationError == null && officeValidationError == null) { + val activationDateInString = formatDate(activationDate) + val submittedOnDateInString = formatDate(submittedOnDate) invokeGroupCreation.invoke( GroupPayloadEntity( @@ -374,64 +386,40 @@ private fun CreateNewGroupContent( ), ) } -// else { -// Toast.makeText( -// context, -// context.resources.getString(R.string.feature_groups_error_not_connected_internet), -// Toast.LENGTH_SHORT, -// ).show() -// } -// } }, ) { - Text(text = stringResource(id = R.string.feature_groups_submit)) + Text(text = stringResource(Res.string.feature_groups_submit)) } } } -private fun validateFields(groupName: String, officeName: String, context: Context): Boolean { +private fun validateGroupField( + groupName: String, +): StringResource? { return when { - groupName.isEmpty() -> { - Toast.makeText( - context, - context.resources.getString(R.string.feature_groups_error_group_name_cannot_be_empty), - Toast.LENGTH_SHORT, - ).show() - return false - } + groupName.isEmpty() -> + Res.string.feature_groups_error_group_name_cannot_be_empty - groupName.trim().length < 4 -> { - Toast.makeText( - context, - context.resources.getString(R.string.feature_groups_error_group_name_must_be_at_least_four_characters_long), - Toast.LENGTH_SHORT, - ).show() - return false - } + groupName.trim().length < 4 -> Res.string.feature_groups_error_group_name_must_be_at_least_four_characters_long - groupName.contains("[^a-zA-Z ]".toRegex()) -> { - Toast.makeText( - context, - context.resources.getString(R.string.feature_groups_error_group_name_should_contain_only_alphabets), - Toast.LENGTH_SHORT, - ).show() - return false - } + groupName.contains("[^a-zA-Z ]".toRegex()) -> Res.string.feature_groups_error_group_name_should_contain_only_alphabets - officeName.isEmpty() -> { - Toast.makeText( - context, - context.resources.getString(R.string.feature_groups_error_office_not_selected), - Toast.LENGTH_SHORT, - ).show() - return false - } + else -> null + } +} + +private fun validateOffice( + officeName: String, +): StringResource? { + return when { + officeName.isEmpty() -> Res.string.feature_groups_error_office_not_selected - else -> true + else -> null } } -private class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider { +private class CreateNewGroupScreenPreviewProvider : + PreviewParameterProvider { override val values: Sequence get() = sequenceOf( CreateNewGroupUiState.ShowProgressbar, @@ -442,7 +430,7 @@ private class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider + when (dataState) { + is DataState.Error -> { + _createNewGroupUiState.value = + CreateNewGroupUiState.ShowFetchingError(dataState.message) + } - repository.offices() - .catch { - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowFetchingError(it.message.toString()) - } - .collect { - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowOffices(it) + DataState.Loading -> { + _createNewGroupUiState.value = + CreateNewGroupUiState.ShowProgressbar + } + + is DataState.Success -> { + val offices = dataState.data + if (offices == null) { + _createNewGroupUiState.value = + CreateNewGroupUiState.ShowFetchingError("No offices found") + } else { + _createNewGroupUiState.value = + CreateNewGroupUiState.ShowOffices(offices) + } + } } + } } } diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/di/GroupsModule.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/di/GroupsModule.kt similarity index 100% rename from feature/groups/src/main/java/com/mifos/feature/groups/di/GroupsModule.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/di/GroupsModule.kt diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt similarity index 89% rename from feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt index 8f4c7d38fa0..81730991f73 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsScreen.kt @@ -9,6 +9,23 @@ */ package com.mifos.feature.groups.groupDetails +import androidclient.feature.groups.generated.resources.Res +import androidclient.feature.groups.generated.resources.feature_groups_accounts +import androidclient.feature.groups.generated.resources.feature_groups_activate_group +import androidclient.feature.groups.generated.resources.feature_groups_activation_date +import androidclient.feature.groups.generated.resources.feature_groups_add_loan_account +import androidclient.feature.groups.generated.resources.feature_groups_add_savings_account +import androidclient.feature.groups.generated.resources.feature_groups_documents +import androidclient.feature.groups.generated.resources.feature_groups_external_id +import androidclient.feature.groups.generated.resources.feature_groups_failed_to_fetch_group_and_account +import androidclient.feature.groups.generated.resources.feature_groups_group +import androidclient.feature.groups.generated.resources.feature_groups_group_clients +import androidclient.feature.groups.generated.resources.feature_groups_loan_account +import androidclient.feature.groups.generated.resources.feature_groups_more_group_info +import androidclient.feature.groups.generated.resources.feature_groups_notes +import androidclient.feature.groups.generated.resources.feature_groups_office +import androidclient.feature.groups.generated.resources.feature_groups_savings_account +import androidclient.feature.groups.generated.resources.feature_groups_staff import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState @@ -31,9 +48,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.DateRange -import androidx.compose.material.icons.outlined.HomeWork -import androidx.compose.material.icons.outlined.Numbers -import androidx.compose.material.icons.outlined.PersonOutline import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -57,14 +71,10 @@ import androidx.compose.ui.graphics.Color.Companion.Black import androidx.compose.ui.graphics.Color.Companion.DarkGray import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.graphics.vector.ImageVector -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.lifecycle.compose.collectAsStateWithLifecycle @@ -74,13 +84,17 @@ import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosMenuDropDownItem import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError -import com.mifos.feature.groups.R +import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity import com.mifos.room.entities.client.ClientEntity import com.mifos.room.entities.group.GroupEntity -import org.koin.androidx.compose.koinViewModel +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.PreviewParameter +import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider +import org.koin.compose.viewmodel.koinViewModel @Composable internal fun GroupDetailsScreen( @@ -156,7 +170,7 @@ internal fun GroupDetailsScreen( MifosScaffold( modifier = modifier, - title = stringResource(id = R.string.feature_groups_group), + title = stringResource(Res.string.feature_groups_group), onBackPressed = onBackPressed, actions = { IconButton(onClick = { showMenu = showMenu.not() }) { @@ -168,42 +182,42 @@ internal fun GroupDetailsScreen( onDismissRequest = { showMenu = false }, ) { MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_add_loan_account), + option = stringResource(Res.string.feature_groups_add_loan_account), onClick = { onMenuClick(MenuItems.ADD_LOAN_ACCOUNT) showMenu = false }, ) MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_add_savings_account), + option = stringResource(Res.string.feature_groups_add_savings_account), onClick = { onMenuClick(MenuItems.ADD_SAVINGS_ACCOUNT) showMenu = false }, ) MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_documents), + option = stringResource(Res.string.feature_groups_documents), onClick = { onMenuClick(MenuItems.DOCUMENTS) showMenu = false }, ) MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_group_clients), + option = stringResource(Res.string.feature_groups_group_clients), onClick = { onMenuClick(MenuItems.GROUP_CLIENTS) showMenu = false }, ) MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_more_group_info), + option = stringResource(Res.string.feature_groups_more_group_info), onClick = { onMenuClick(MenuItems.MORE_GROUP_INFO) showMenu = false }, ) MifosMenuDropDownItem( - option = stringResource(id = R.string.feature_groups_notes), + option = stringResource(Res.string.feature_groups_notes), onClick = { onMenuClick(MenuItems.NOTES) showMenu = false @@ -220,12 +234,9 @@ internal fun GroupDetailsScreen( .fillMaxWidth() .heightIn(44.dp) .padding(start = 16.dp, end = 16.dp), -// colors = ButtonDefaults.buttonColors( -// containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, -// ), ) { Text( - text = stringResource(id = R.string.feature_groups_activate_group), + text = stringResource(Res.string.feature_groups_activate_group), fontSize = 16.sp, ) } @@ -234,7 +245,7 @@ internal fun GroupDetailsScreen( ) { paddingValue -> Column(modifier = Modifier.padding(paddingValue)) { when (state) { - is GroupDetailsUiState.Error -> MifosSweetError(message = stringResource(id = state.message)) { + is GroupDetailsUiState.Error -> MifosSweetError(message = stringResource(state.message)) { } is GroupDetailsUiState.Loading -> MifosCircularProgress() @@ -286,27 +297,27 @@ fun GroupDetailsContent( } group.externalId?.let { MifosCenterDetailsText( - icon = Icons.Outlined.Numbers, - field = stringResource(id = R.string.feature_groups_external_id), + icon = MifosIcons.Numbers, + field = stringResource(Res.string.feature_groups_external_id), value = it, ) } MifosCenterDetailsText( icon = Icons.Outlined.DateRange, - field = stringResource(id = R.string.feature_groups_activation_date), + field = stringResource(Res.string.feature_groups_activation_date), value = Utils.getStringOfDate(group.activationDate), ) group.officeName?.let { MifosCenterDetailsText( - icon = Icons.Outlined.HomeWork, - field = stringResource(id = R.string.feature_groups_office), + icon = MifosIcons.Homework, + field = stringResource(Res.string.feature_groups_office), value = it, ) } group.staffName?.let { MifosCenterDetailsText( - icon = Icons.Outlined.PersonOutline, - field = stringResource(id = R.string.feature_groups_staff), + icon = MifosIcons.PersonOutline, + field = stringResource(Res.string.feature_groups_staff), value = it, ) } @@ -314,7 +325,7 @@ fun GroupDetailsContent( if (loanAccounts.isNotEmpty() || savingsAccounts.isNotEmpty()) { Text( modifier = Modifier.padding(start = 16.dp), - text = stringResource(id = R.string.feature_groups_accounts), + text = stringResource(Res.string.feature_groups_accounts), style = TextStyle( fontSize = 21.sp, fontWeight = FontWeight.Medium, @@ -327,14 +338,14 @@ fun GroupDetailsContent( } if (loanAccounts.isNotEmpty()) { MifosLoanAccountExpendableCard( - stringResource(id = R.string.feature_groups_loan_account), + stringResource(Res.string.feature_groups_loan_account), loanAccounts, loanAccountSelected = loanAccountSelected, ) } if (savingsAccounts.isNotEmpty()) { MifosSavingsAccountExpendableCard( - stringResource(id = R.string.feature_groups_savings_account), + stringResource(Res.string.feature_groups_savings_account), savingsAccounts, savingsAccountSelected = savingsAccountSelected, ) @@ -748,12 +759,29 @@ private fun MifosSavingsAccountsLazyColumn( } } +enum class MenuItems { + ADD_LOAN_ACCOUNT, + ADD_SAVINGS_ACCOUNT, + DOCUMENTS, + GROUP_CLIENTS, + MORE_GROUP_INFO, + NOTES, +} + +private val sampleLoanAccountList = List(10) { + LoanAccountEntity(id = it, productName = "Product $it") +} + +private val sampleSavingAccountList = List(10) { + SavingsAccountEntity(id = it, productName = "Product $it") +} + private class GroupDetailsUiStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( GroupDetailsUiState.Loading, - GroupDetailsUiState.Error(R.string.feature_groups_failed_to_fetch_group_and_account), + GroupDetailsUiState.Error(Res.string.feature_groups_failed_to_fetch_group_and_account), GroupDetailsUiState.ShowGroup(group = GroupEntity(name = "Group", active = true)), GroupDetailsUiState.ShowGroup(group = GroupEntity(name = "Group", active = false)), ) @@ -776,20 +804,3 @@ private fun GroupDetailsScreenPreview( activateGroup = {}, ) } - -enum class MenuItems { - ADD_LOAN_ACCOUNT, - ADD_SAVINGS_ACCOUNT, - DOCUMENTS, - GROUP_CLIENTS, - MORE_GROUP_INFO, - NOTES, -} - -private val sampleLoanAccountList = List(10) { - LoanAccountEntity(id = it, productName = "Product $it") -} - -private val sampleSavingAccountList = List(10) { - SavingsAccountEntity(id = it, productName = "Product $it") -} diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt similarity index 83% rename from feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt index f52af0d347c..5830f0dff67 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsUiState.kt @@ -10,6 +10,7 @@ package com.mifos.feature.groups.groupDetails import com.mifos.room.entities.group.GroupEntity +import org.jetbrains.compose.resources.StringResource /** * Created by Aditya Gupta on 06/08/23. @@ -18,7 +19,7 @@ sealed class GroupDetailsUiState { data object Loading : GroupDetailsUiState() - data class Error(val message: Int) : GroupDetailsUiState() + data class Error(val message: StringResource) : GroupDetailsUiState() data class ShowGroup(val group: GroupEntity) : GroupDetailsUiState() } diff --git a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt new file mode 100644 index 00000000000..4f433dbaaaf --- /dev/null +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.feature.groups.groupDetails + +import androidclient.feature.groups.generated.resources.Res +import androidclient.feature.groups.generated.resources.feature_groups_failed_to_fetch_group_and_account +import androidclient.feature.groups.generated.resources.feature_groups_failed_to_load_client +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.Constants +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repository.GroupDetailsRepository +import com.mifos.core.domain.useCases.GetGroupDetailsUseCase +import com.mifos.room.entities.accounts.loans.LoanAccountEntity +import com.mifos.room.entities.accounts.savings.SavingsAccountEntity +import com.mifos.room.entities.client.ClientEntity +import com.mifos.room.entities.group.GroupEntity +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class GroupDetailsViewModel( + private val savedStateHandle: SavedStateHandle, + private val repository: GroupDetailsRepository, + private val getGroupDetailsUseCase: GetGroupDetailsUseCase, +) : ViewModel() { + + val groupId = savedStateHandle.getStateFlow(key = Constants.GROUP_ID, initialValue = 0) + + private val _groupDetailsUiState = + MutableStateFlow(GroupDetailsUiState.Loading) + val groupDetailsUiState = _groupDetailsUiState.asStateFlow() + + private val _loanAccounts = MutableStateFlow>(emptyList()) + val loanAccounts = _loanAccounts.asStateFlow() + + private val _savingsAccounts = MutableStateFlow>(emptyList()) + val savingsAccounts = _savingsAccounts.asStateFlow() + + private val _groupAssociateClients = MutableStateFlow>(emptyList()) + val groupAssociateClients = _groupAssociateClients.asStateFlow() + + fun getGroupDetails(groupId: Int) { + viewModelScope.launch { + getGroupDetailsUseCase.invoke(groupId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + _groupDetailsUiState.value = + GroupDetailsUiState.Error(Res.string.feature_groups_failed_to_fetch_group_and_account) + } + + DataState.Loading -> + _groupDetailsUiState.value = + GroupDetailsUiState.Loading + + is DataState.Success -> { + val account = dataState.data + _groupDetailsUiState.value = + GroupDetailsUiState.ShowGroup(account.group ?: GroupEntity()) + _loanAccounts.value = + account.groupAccounts?.loanAccounts ?: emptyList() + _savingsAccounts.value = + account.groupAccounts?.savingsAccounts ?: emptyList() + } + } + } + } + } + + fun getGroupAssociateClients(groupId: Int) { + viewModelScope.launch { + repository.getGroupWithAssociations(groupId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + _groupDetailsUiState.value = + GroupDetailsUiState.Error(Res.string.feature_groups_failed_to_load_client) + } + + DataState.Loading -> Unit + + is DataState.Success -> { + _groupAssociateClients.value = + dataState.data.clientMembers + } + } + } + } + } +} diff --git a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.kt new file mode 100644 index 00000000000..55efa9f0575 --- /dev/null +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.feature.groups.groupList + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.room.entities.group.GroupEntity +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal expect fun GroupsListRoute( + paddingValues: PaddingValues, + onAddGroupClick: () -> Unit, + onGroupClick: (groupId: Int) -> Unit, + viewModel: GroupsListViewModel = koinViewModel(), +) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun GroupItem( + group: GroupEntity, + doesSelected: Boolean, + inSelectionMode: Boolean, + onGroupClick: () -> Unit, + modifier: Modifier = Modifier, + onSelectItem: () -> Unit, +) { + val borderStroke = if (doesSelected) { + BorderStroke(1.dp, Color.Blue) + } else { + CardDefaults.outlinedCardBorder() + } + val containerColor = if (doesSelected) Color.Blue else Color.Unspecified + + group.name?.let { + OutlinedCard( + modifier = modifier + .testTag(it) + .fillMaxWidth() + .padding(8.dp) + .height(70.dp) + .clip(RoundedCornerShape(8.dp)) + .combinedClickable( + onClick = { + if (inSelectionMode) { + onSelectItem() + } else { + onGroupClick() + } + }, + onLongClick = onSelectItem, + ), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = containerColor, + ), + border = borderStroke, + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + ) + + if (group.sync) { + Icon(imageVector = MifosIcons.DoneAll, contentDescription = "Sync") + } + } + } + } +} diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListViewModel.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupList/GroupsListViewModel.kt similarity index 100% rename from feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListViewModel.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/groupList/GroupsListViewModel.kt diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt similarity index 99% rename from feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt index 04f666da628..dab727e38b1 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt @@ -27,7 +27,6 @@ import com.mifos.room.entities.client.ClientEntity /** * Created by Pronay Sarker on 13/08/2024 */ - fun NavGraphBuilder.groupNavGraph( paddingValues: PaddingValues, navController: NavController, @@ -43,7 +42,7 @@ fun NavGraphBuilder.groupNavGraph( ) { navigation( startDestination = GroupScreen.GroupListScreen.route, - route = "group_list_routes", + route = "groups_screen_graph", ) { groupListScreenRoute( paddingValues = paddingValues, diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupScreen.kt similarity index 91% rename from feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupScreen.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupScreen.kt index b3162720fc8..132e8b60c1e 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupScreen.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupScreen.kt @@ -16,7 +16,7 @@ import com.mifos.core.common.utils.Constants */ sealed class GroupScreen(val route: String) { - data object GroupListScreen : GroupScreen("group_list_screen") + data object GroupListScreen : GroupScreen("groups_screen") data object CreateNewGroupScreen : GroupScreen("create_new_group") diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt similarity index 70% rename from feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt index 4581eb4c80e..5540728f87e 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupDialogScreen.kt @@ -9,6 +9,21 @@ */ package com.mifos.feature.groups.syncGroupDialog +import androidclient.feature.groups.generated.resources.Res +import androidclient.feature.groups.generated.resources.feature_groups_cancel +import androidclient.feature.groups.generated.resources.feature_groups_dialog_action_ok +import androidclient.feature.groups.generated.resources.feature_groups_failed_sync +import androidclient.feature.groups.generated.resources.feature_groups_groups +import androidclient.feature.groups.generated.resources.feature_groups_hide +import androidclient.feature.groups.generated.resources.feature_groups_name +import androidclient.feature.groups.generated.resources.feature_groups_slash +import androidclient.feature.groups.generated.resources.feature_groups_something_went_wrong +import androidclient.feature.groups.generated.resources.feature_groups_space +import androidclient.feature.groups.generated.resources.feature_groups_sync_groups_full_information +import androidclient.feature.groups.generated.resources.feature_groups_syncing_client +import androidclient.feature.groups.generated.resources.feature_groups_syncing_group +import androidclient.feature.groups.generated.resources.feature_groups_total +import androidclient.feature.groups.generated.resources.feature_groups_total_sync_progress import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,15 +45,14 @@ import androidx.compose.runtime.remember 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.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mifos.core.designsystem.component.MifosCircularProgress -import com.mifos.feature.groups.R -import org.koin.androidx.compose.koinViewModel +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel @Composable internal fun SyncGroupDialogScreen( @@ -87,7 +101,7 @@ internal fun SyncGroupDialogScreen( is SyncGroupsDialogUiState.Error -> { val message = uiState.message ?: uiState.messageResId?.let { stringResource(uiState.messageResId) } - ?: stringResource(id = R.string.feature_groups_something_went_wrong) + ?: stringResource(Res.string.feature_groups_something_went_wrong) LaunchedEffect(key1 = message) { snackBarHostState.showSnackbar(message = message) } @@ -115,25 +129,27 @@ private fun SyncGroupDialogContent( Text( modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, - text = stringResource(id = R.string.feature_groups_sync_groups_full_information), + text = stringResource(Res.string.feature_groups_sync_groups_full_information), ) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_name), + label = stringResource(Res.string.feature_groups_name), value = uiData.groupName, ) Spacer(modifier = Modifier.height(12.dp)) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_total), - value = uiData.groupList.size.toString() + stringResource(R.string.feature_groups_space) + stringResource(R.string.feature_groups_groups), + label = stringResource(Res.string.feature_groups_total), + value = uiData.groupList.size.toString() + stringResource(Res.string.feature_groups_space) + stringResource( + Res.string.feature_groups_groups, + ), ) Spacer(modifier = Modifier.height(12.dp)) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_syncing_group), + label = stringResource(Res.string.feature_groups_syncing_group), value = uiData.groupName, ) @@ -144,7 +160,7 @@ private fun SyncGroupDialogContent( Spacer(modifier = Modifier.height(12.dp)) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_syncing_client), + label = stringResource(Res.string.feature_groups_syncing_client), value = "syncing_client", ) @@ -155,8 +171,9 @@ private fun SyncGroupDialogContent( Spacer(modifier = Modifier.height(12.dp)) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_total_sync_progress), - value = stringResource(R.string.feature_groups_space) + uiData.totalSyncCount + stringResource(id = R.string.feature_groups_slash) + uiData.groupList.size, + label = stringResource(Res.string.feature_groups_total_sync_progress), + value = stringResource(Res.string.feature_groups_space) + uiData.totalSyncCount + + stringResource(Res.string.feature_groups_slash) + uiData.groupList.size, ) LinearProgressIndicator( @@ -166,7 +183,7 @@ private fun SyncGroupDialogContent( Spacer(modifier = Modifier.height(12.dp)) GroupPayloadField( - label = stringResource(id = R.string.feature_groups_failed_sync), + label = stringResource(Res.string.feature_groups_failed_sync), value = uiData.failedSyncGroupCount.toString(), ) @@ -180,14 +197,14 @@ private fun SyncGroupDialogContent( onClick = { okClicked() }, modifier = Modifier.weight(1f), ) { - Text(text = stringResource(id = R.string.feature_groups_dialog_action_ok)) + Text(text = stringResource(Res.string.feature_groups_dialog_action_ok)) } } else { FilledTonalButton( onClick = { cancelClicked() }, modifier = Modifier.weight(1f), ) { - Text(text = stringResource(id = R.string.feature_groups_cancel)) + Text(text = stringResource(Res.string.feature_groups_cancel)) } Spacer(modifier = Modifier.width(10.dp)) @@ -196,7 +213,7 @@ private fun SyncGroupDialogContent( onClick = { hideClicked() }, modifier = Modifier.weight(1f), ) { - Text(text = stringResource(id = R.string.feature_groups_hide)) + Text(text = stringResource(Res.string.feature_groups_hide)) } } } @@ -228,8 +245,8 @@ private fun GroupPayloadField( } } -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Composable +@Preview private fun SyncGroupDialogScreenPreview() { SyncGroupDialogScreen( dismiss = { }, diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt similarity index 91% rename from feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt index d315c2941f5..72f586596b1 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogUiState.kt @@ -11,6 +11,7 @@ package com.mifos.feature.groups.syncGroupDialog import androidx.compose.ui.graphics.vector.ImageVector import com.mifos.room.entities.group.GroupEntity +import org.jetbrains.compose.resources.StringResource /** * Created by Aditya Gupta on 16/08/23. @@ -19,7 +20,7 @@ sealed class SyncGroupsDialogUiState { data object Loading : SyncGroupsDialogUiState() data object Success : SyncGroupsDialogUiState() data class Error( - val messageResId: Int? = null, + val messageResId: StringResource? = null, val imageVector: ImageVector? = null, val message: String? = null, ) : SyncGroupsDialogUiState() diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt similarity index 65% rename from feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt rename to feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt index a97b735b50e..0e4d386da1d 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/syncGroupDialog/SyncGroupsDialogViewModel.kt @@ -9,32 +9,26 @@ */ package com.mifos.feature.groups.syncGroupDialog -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import com.mifos.core.common.utils.Constants +import com.mifos.core.common.utils.DataState import com.mifos.core.data.repository.SyncGroupsDialogRepository import com.mifos.core.datastore.UserPreferencesRepository +import com.mifos.core.domain.useCases.GetLoanAndLoanRepaymentUseCase +import com.mifos.core.domain.useCases.GetSavingsAccountAndTemplateUseCase import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity import com.mifos.room.entities.client.ClientEntity import com.mifos.room.entities.group.GroupEntity -import com.mifos.room.entities.zipmodels.LoanAndLoanRepayment -import com.mifos.room.entities.zipmodels.SavingsAccountAndTransactionTemplate import io.ktor.client.plugins.ClientRequestException import io.ktor.client.plugins.ServerResponseException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import rx.Observable -import rx.plugins.RxJavaPlugins /** * Created by Aditya Gupta on 16/08/23. @@ -42,6 +36,8 @@ import rx.plugins.RxJavaPlugins class SyncGroupsDialogViewModel( private val repository: SyncGroupsDialogRepository, private val prefManager: UserPreferencesRepository, + private val getSavingsAccountAndTemplateUseCase: GetSavingsAccountAndTemplateUseCase, + private val getLoanAndLoanRepaymentUseCase: GetLoanAndLoanRepaymentUseCase, // private val networkUtilsWrapper: NetworkUtilsWrapper, ) : ViewModel() { @@ -182,9 +178,9 @@ class SyncGroupsDialogViewModel( _syncGroupData.update { it.copy(failedSyncGroupCount = mFailedSyncGroup.size) } syncGroups() } - Log.d("Error", e.toString()) + Logger.d("debug", Throwable(e.toString())) } catch (throwable: Throwable) { - RxJavaPlugins.getInstance().errorHandler.handleError(throwable) + Logger.d("debug", throwable) } } @@ -204,21 +200,28 @@ class SyncGroupsDialogViewModel( private fun syncGroupAccounts(groupId: Int) { viewModelScope.launch { repository.syncGroupAccounts(groupId) - .catch { - onAccountSyncFailed(it) - } - .collect { groupAccounts -> - mLoanAccountList = getActiveLoanAccounts( - groupAccounts.loanAccounts, - ) - mSavingsAccountList = getActiveSavingsAccounts( - groupAccounts.savingsAccounts, - ) - - // Updating UI - maxSingleSyncGroupProgressBar = - mLoanAccountList.size + mSavingsAccountList.size - checkAccountsSyncStatusAndSyncAccounts() + .collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> Unit + + is DataState.Success -> { + val groupAccounts = dataState.data + + mLoanAccountList = getActiveLoanAccounts( + groupAccounts.loanAccounts, + ) + mSavingsAccountList = getActiveSavingsAccounts( + groupAccounts.savingsAccounts, + ) + + // Updating UI + maxSingleSyncGroupProgressBar = + mLoanAccountList.size + mSavingsAccountList.size + checkAccountsSyncStatusAndSyncAccounts() + } + } } } } @@ -234,19 +237,24 @@ class SyncGroupsDialogViewModel( */ private fun syncLoanAndLoanRepayment(loanId: Int) { viewModelScope.launch { - getLoanAndLoanRepaymentNew(loanId) - .flowOn(Dispatchers.IO) - .catch { e -> - onAccountSyncFailed(e) - }.collect { - mLoanAndRepaymentSyncIndex += 1 - _syncGroupData.update { it.copy(singleSyncCount = mLoanAndRepaymentSyncIndex) } - - if (mLoanAndRepaymentSyncIndex != mLoanAccountList.size) { - checkNetworkConnectionAndSyncLoanAndLoanRepayment() - } else { - setLoanAccountSyncStatusTrue() - checkAccountsSyncStatusAndSyncAccounts() + getLoanAndLoanRepaymentUseCase.invoke(loanId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> Unit + + is DataState.Success -> { + mLoanAndRepaymentSyncIndex += 1 + _syncGroupData.update { it.copy(singleSyncCount = mLoanAndRepaymentSyncIndex) } + + if (mLoanAndRepaymentSyncIndex != mLoanAccountList.size) { + checkNetworkConnectionAndSyncLoanAndLoanRepayment() + } else { + setLoanAccountSyncStatusTrue() + checkAccountsSyncStatusAndSyncAccounts() + } + } } } } @@ -261,18 +269,26 @@ class SyncGroupsDialogViewModel( */ private fun syncSavingsAccountAndTemplate(savingsAccountType: String, savingsAccountId: Int) { viewModelScope.launch { - getSavingsAccountAndTemplate(savingsAccountType, savingsAccountId) - .catch { - onAccountSyncFailed(it) - }.collect { - mSavingsAndTransactionSyncIndex += 1 - _syncGroupData.update { it.copy(singleSyncCount = mLoanAndRepaymentSyncIndex + mSavingsAndTransactionSyncIndex) } - if (mSavingsAndTransactionSyncIndex != mSavingsAccountList.size) { - checkNetworkConnectionAndSyncSavingsAccountAndTransactionTemplate() - } else { - mGroupList[mGroupSyncIndex].id?.let { loadGroupAssociateClients(it) } + getSavingsAccountAndTemplateUseCase.invoke( + savingsAccountType, + savingsAccountId, + ).collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> Unit + + is DataState.Success -> { + mSavingsAndTransactionSyncIndex += 1 + _syncGroupData.update { it.copy(singleSyncCount = mLoanAndRepaymentSyncIndex + mSavingsAndTransactionSyncIndex) } + if (mSavingsAndTransactionSyncIndex != mSavingsAccountList.size) { + checkNetworkConnectionAndSyncSavingsAccountAndTransactionTemplate() + } else { + mGroupList[mGroupSyncIndex].id?.let { loadGroupAssociateClients(it) } + } } } + } } } @@ -282,20 +298,28 @@ class SyncGroupsDialogViewModel( * @param groupId Group Id */ private fun loadGroupAssociateClients(groupId: Int) { - _syncGroupsDialogUiState.value = SyncGroupsDialogUiState.Loading viewModelScope.launch { repository.getGroupWithAssociations(groupId) - .catch { - onAccountSyncFailed(it) - }.collect { groupWithAssociations -> - mClients = groupWithAssociations.clientMembers - mClientSyncIndex = 0 - resetIndexes() - if (mClients.isNotEmpty()) { - _syncGroupData.update { it.copy(totalClientSyncCount = mClients.size) } - syncClientAccounts(mClients[mClientSyncIndex].id) - } else { - syncGroup(mGroupList[mGroupSyncIndex]) + .collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> + _syncGroupsDialogUiState.value = + SyncGroupsDialogUiState.Loading + + is DataState.Success -> { + val groupWithAssociations = dataState.data + mClients = groupWithAssociations.clientMembers + mClientSyncIndex = 0 + resetIndexes() + if (mClients.isNotEmpty()) { + _syncGroupData.update { it.copy(totalClientSyncCount = mClients.size) } + syncClientAccounts(mClients[mClientSyncIndex].id) + } else { + syncGroup(mGroupList[mGroupSyncIndex]) + } + } } } } @@ -307,7 +331,7 @@ class SyncGroupsDialogViewModel( * * @param client */ - private fun syncClient(client: com.mifos.room.entities.client.ClientEntity) { + private fun syncClient(client: ClientEntity) { val updatedClient = client.copy( groupId = mGroupList[mGroupSyncIndex].id, sync = true, @@ -368,7 +392,7 @@ class SyncGroupsDialogViewModel( * * @param clientId Client Id */ - private fun syncClientAccounts(clientId: Int) = viewModelScope.launch(Dispatchers.IO) { + private fun syncClientAccounts(clientId: Int) = viewModelScope.launch { val clientAccounts = repository.syncClientAccounts(clientId) mLoanAccountList = getActiveLoanAccounts( clientAccounts @@ -392,42 +416,27 @@ class SyncGroupsDialogViewModel( */ private fun syncClientLoanAndLoanRepayment(loanId: Int) { viewModelScope.launch { - try { - getLoanAndLoanRepaymentNew(loanId) - - mLoanAndRepaymentSyncIndex += 1 - if (mLoanAndRepaymentSyncIndex != mLoanAccountList.size) { - mLoanAccountList[mLoanAndRepaymentSyncIndex].id?.let { - syncClientLoanAndLoanRepayment(it) + getLoanAndLoanRepaymentUseCase.invoke(loanId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> Unit + + is DataState.Success -> { + mLoanAndRepaymentSyncIndex += 1 + if (mLoanAndRepaymentSyncIndex != mLoanAccountList.size) { + mLoanAccountList[mLoanAndRepaymentSyncIndex].id?.let { + syncClientLoanAndLoanRepayment(it) + } + } else { + setLoanAccountSyncStatusTrue() + checkAccountsSyncStatusAndSyncClientAccounts() + } + } } - } else { - setLoanAccountSyncStatusTrue() - checkAccountsSyncStatusAndSyncClientAccounts() } - } catch (e: Exception) { - onAccountSyncFailed(e) - } } -// getLoanAndLoanRepayment(loanId) -// .subscribe( -// object : Subscriber() { -// override fun onCompleted() {} -// override fun onError(e: Throwable) { -// } -// -// override fun onNext(loanAndLoanRepayment: LoanAndLoanRepayment) { -// mLoanAndRepaymentSyncIndex += 1 -// if (mLoanAndRepaymentSyncIndex != mLoanAccountList.size) { -// mLoanAccountList[mLoanAndRepaymentSyncIndex].id?.let { -// syncClientLoanAndLoanRepayment(it) -// } -// } else { -// setLoanAccountSyncStatusTrue() -// checkAccountsSyncStatusAndSyncClientAccounts() -// } -// } -// }, -// ) } /** @@ -442,25 +451,33 @@ class SyncGroupsDialogViewModel( savingsAccountId: Int, ) { viewModelScope.launch { - getSavingsAccountAndTemplate(savingsAccountType, savingsAccountId) - .catch { - onAccountSyncFailed(it) - }.collect { - mSavingsAndTransactionSyncIndex += 1 - if (mSavingsAndTransactionSyncIndex != mSavingsAccountList.size) { - mSavingsAccountList[mSavingsAndTransactionSyncIndex] - .depositType?.endpoint?.let { - mSavingsAccountList[mSavingsAndTransactionSyncIndex].id?.let { it1 -> - syncClientSavingsAccountAndTemplate( - it, - it1, - ) + getSavingsAccountAndTemplateUseCase.invoke( + savingsAccountType, + savingsAccountId, + ).collect { dataState -> + when (dataState) { + is DataState.Error -> onAccountSyncFailed(dataState.exception) + + DataState.Loading -> Unit + + is DataState.Success -> { + mSavingsAndTransactionSyncIndex += 1 + if (mSavingsAndTransactionSyncIndex != mSavingsAccountList.size) { + mSavingsAccountList[mSavingsAndTransactionSyncIndex] + .depositType?.endpoint?.let { + mSavingsAccountList[mSavingsAndTransactionSyncIndex].id?.let { it1 -> + syncClientSavingsAccountAndTemplate( + it, + it1, + ) + } } - } - } else { - syncClient(mClients[mClientSyncIndex]) + } else { + syncClient(mClients[mClientSyncIndex]) + } } } + } } } @@ -485,54 +502,6 @@ class SyncGroupsDialogViewModel( } } - /** - * This Method Fetching the LoanAndLoanRepayment - * - * @param loanId Loan Id - * @return LoanAndLoanRepayment - */ - private fun getLoanAndLoanRepaymentNew(loanId: Int): Flow { - return combine( - repository.syncLoanById(loanId), - repository.syncLoanRepaymentTemplate(loanId), - ) { loanWithAssociations, loanRepaymentTemplate -> - LoanAndLoanRepayment( - loanWithAssociations, - loanRepaymentTemplate, - ) - }.flowOn(Dispatchers.IO) - } - - /** - * This method fetching SavingsAccountAndTemplate. - * - * @param savingsAccountType - * @param savingsAccountId - * @return SavingsAccountAndTransactionTemplate - */ - private fun getSavingsAccountAndTemplate( - savingsAccountType: String, - savingsAccountId: Int, - ): Flow { - return combine( - repository.syncSavingsAccount( - savingsAccountType, - savingsAccountId, - Constants.TRANSACTIONS, - ), - repository.syncSavingsAccountTransactionTemplate( - savingsAccountType, - savingsAccountId, - Constants.SAVINGS_ACCOUNT_TRANSACTION_DEPOSIT, - ), - ) { savingsAccountWithAssociations, savingsAccountTransactionTemplate -> - SavingsAccountAndTransactionTemplate( - savingsAccountWithAssociations, - savingsAccountTransactionTemplate, - ) - } - } - private fun updateTotalSyncProgressBarAndCount() { _syncGroupData.update { it.copy(totalSyncCount = mGroupSyncIndex) } } @@ -558,33 +527,23 @@ class SyncGroupsDialogViewModel( } fun getActiveLoanAccounts(loanAccountList: List?): List { - val loanAccounts: MutableList = ArrayList() - Observable.from(loanAccountList) - .filter { loanAccount -> loanAccount.status?.active } - .subscribe { loanAccount -> loanAccounts.add(loanAccount) } - return loanAccounts + return loanAccountList + ?.filter { it.status?.active == true } + ?: emptyList() } fun getActiveSavingsAccounts(savingsAccounts: List?): List { - val accounts: MutableList = ArrayList() - Observable.from(savingsAccounts) - .filter { savingsAccount -> - savingsAccount.status?.active == true && - !savingsAccount.depositType!!.isRecurring - } - .subscribe { savingsAccount -> accounts.add(savingsAccount) } - return accounts + return savingsAccounts + ?.filter { it.status?.active == true && it.depositType?.isRecurring == false } + ?: emptyList() } fun getSyncableSavingsAccounts(savingsAccounts: List?): List { - val accounts: MutableList = ArrayList() - Observable.from(savingsAccounts) - .filter { savingsAccount -> - savingsAccount.depositType?.value == "Savings" && - savingsAccount.status?.active == true && - !savingsAccount.depositType!!.isRecurring - } - .subscribe { savingsAccount -> accounts.add(savingsAccount) } - return accounts + return savingsAccounts + ?.filter { + it.depositType?.value == "Savings" && + it.status?.active == true && + it.depositType?.isRecurring == false + } ?: emptyList() } } diff --git a/feature/groups/src/desktopMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.desktop.kt b/feature/groups/src/desktopMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.desktop.kt new file mode 100644 index 00000000000..19f01e0095b --- /dev/null +++ b/feature/groups/src/desktopMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.desktop.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.feature.groups.groupList + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +internal actual fun GroupsListRoute( + paddingValues: androidx.compose.foundation.layout.PaddingValues, + onAddGroupClick: () -> Unit, + onGroupClick: (Int) -> Unit, + viewModel: GroupsListViewModel, +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Not available in desktop yet", + ) + } +} diff --git a/feature/groups/src/main/AndroidManifest.xml b/feature/groups/src/main/AndroidManifest.xml deleted file mode 100644 index 6ff297023f0..00000000000 --- a/feature/groups/src/main/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt b/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt deleted file mode 100644 index a7eb36a20d4..00000000000 --- a/feature/groups/src/main/java/com/mifos/feature/groups/groupDetails/GroupDetailsViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.groups.groupDetails - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mifos.core.common.utils.Constants -import com.mifos.core.data.repository.GroupDetailsRepository -import com.mifos.feature.groups.R -import com.mifos.room.entities.accounts.loans.LoanAccountEntity -import com.mifos.room.entities.accounts.savings.SavingsAccountEntity -import com.mifos.room.entities.client.ClientEntity -import com.mifos.room.entities.group.GroupEntity -import com.mifos.room.entities.zipmodels.GroupAndGroupAccounts -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch - -class GroupDetailsViewModel( - private val savedStateHandle: SavedStateHandle, - private val repository: GroupDetailsRepository, -) : ViewModel() { - - val groupId = savedStateHandle.getStateFlow(key = Constants.GROUP_ID, initialValue = 0) - - private val _groupDetailsUiState = - MutableStateFlow(GroupDetailsUiState.Loading) - val groupDetailsUiState = _groupDetailsUiState.asStateFlow() - - private val _loanAccounts = MutableStateFlow>(emptyList()) - val loanAccounts = _loanAccounts.asStateFlow() - - private val _savingsAccounts = MutableStateFlow>(emptyList()) - val savingsAccounts = _savingsAccounts.asStateFlow() - - private val _groupAssociateClients = MutableStateFlow>(emptyList()) - val groupAssociateClients = _groupAssociateClients.asStateFlow() - - fun getGroupDetails(groupId: Int) { - viewModelScope.launch { - _groupDetailsUiState.value = GroupDetailsUiState.Loading - combine( - repository.getGroup(groupId), - repository.getGroupAccounts(groupId), - ) { group, groupAccounts -> - GroupAndGroupAccounts(group, groupAccounts) - }.catch { - _groupDetailsUiState.value = - GroupDetailsUiState.Error(R.string.feature_groups_failed_to_fetch_group_and_account) - }.collect { account -> - _groupDetailsUiState.value = - GroupDetailsUiState.ShowGroup(account.group ?: GroupEntity()) - _loanAccounts.value = account.groupAccounts?.loanAccounts ?: emptyList() - _savingsAccounts.value = account.groupAccounts?.savingsAccounts ?: emptyList() - } - } - } - - fun getGroupAssociateClients(groupId: Int) { - viewModelScope.launch { - repository.getGroupWithAssociations(groupId) - .catch { - _groupDetailsUiState.value = - GroupDetailsUiState.Error(R.string.feature_groups_failed_to_load_client) - } - .collect { - _groupAssociateClients.value = it.clientMembers ?: emptyList() - } - } - } -} diff --git a/feature/groups/src/nativeMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.native.kt b/feature/groups/src/nativeMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.native.kt new file mode 100644 index 00000000000..d89f00ccaad --- /dev/null +++ b/feature/groups/src/nativeMain/kotlin/com/mifos/feature/groups/groupList/GroupsListScreen.native.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.feature.groups.groupList + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +internal actual fun GroupsListRoute( + paddingValues: PaddingValues, + onAddGroupClick: () -> Unit, + onGroupClick: (Int) -> Unit, + viewModel: GroupsListViewModel, +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Not available in IOS yet", + ) + } +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/NoteScreen.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/NoteScreen.kt index f97697f8dcf..7fa49132ad0 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/NoteScreen.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/NoteScreen.kt @@ -126,7 +126,7 @@ private fun NoteContent( ) { LazyColumn(modifier = modifier) { items(notes) { note -> - note.noteContent?.let { NoteItem(noteTitle = it) } + note.note?.let { NoteItem(noteTitle = it) } } } } @@ -163,7 +163,7 @@ internal val demoNotes = listOf( Note( id = 1, clientId = 101, - noteContent = "This is the first demo note.", + note = "This is the first demo note.", createdById = 1001, createdByUsername = "creator_1", createdOn = Clock.System.now().toEpochMilliseconds(), @@ -174,7 +174,7 @@ internal val demoNotes = listOf( Note( id = 2, clientId = 102, - noteContent = "This is the second demo note.", + note = "This is the second demo note.", createdById = 1003, createdByUsername = "creator_2", createdOn = Clock.System.now().toEpochMilliseconds(), @@ -185,7 +185,7 @@ internal val demoNotes = listOf( Note( id = 3, clientId = 103, - noteContent = "This is the third demo note.", + note = "This is the third demo note.", createdById = 1005, createdByUsername = "creator_3", createdOn = Clock.System.now().toEpochMilliseconds(), diff --git a/settings.gradle.kts b/settings.gradle.kts index a0c77ec3222..60bceea1e88 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -54,7 +54,7 @@ include(":feature:checker-inbox-task") include(":feature:collectionSheet") //include(":feature:data-table") //include(":feature:document") -//include(":feature:groups") +include(":feature:groups") //include(":feature:loan") include(":feature:note") //include(":feature:offline")