diff --git a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/ClientAccounts.kt b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/ClientAccounts.kt index 730a1ccf806..8ecc36c5096 100644 --- a/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/ClientAccounts.kt +++ b/core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/ClientAccounts.kt @@ -19,6 +19,7 @@ data class ClientAccounts( var loanAccounts: List = emptyList(), var savingsAccounts: List = emptyList(), + ) : Parcelable { private fun getSavingsAccounts(wantRecurring: Boolean): List { val result: MutableList = ArrayList() 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 223daa9972a..a3593e8623c 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 @@ -71,6 +71,8 @@ data class SavingsAccountEntity( val productId: Int? = null, + val shortProductName: String? = null, + val productName: String? = null, @ColumnInfo(index = true, name = INHERIT_FIELD_NAME, typeAffinity = UNDEFINED, collate = UNSPECIFIED, defaultValue = VALUE_UNSPECIFIED) diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateChargesUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateChargesUseCase.kt index 6993bf36d76..6a7c6585f57 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateChargesUseCase.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateChargesUseCase.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.flow class CreateChargesUseCase( private val repository: ChargeDialogRepository, ) { - operator fun invoke( clientId: Int, payload: ChargesPayload, diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetClientsClientIdAccountMapper.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetClientsClientIdAccountMapper.kt index 676293ed411..cc6436238c7 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetClientsClientIdAccountMapper.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetClientsClientIdAccountMapper.kt @@ -39,7 +39,9 @@ object GetClientsClientIdAccountMapper : SavingsAccountEntity( id = it.id?.toInt(), accountNo = it.accountNo, + accountBalance = it.accountBalance, productId = it.productId?.toInt(), + shortProductName = it.shortProductName, productName = it.productName, depositType = it.depositType?.let { deposit -> SavingAccountDepositTypeEntity( @@ -115,6 +117,7 @@ object GetClientsClientIdAccountMapper : GetClientsSavingsAccounts( id = it.id?.toLong(), accountNo = it.accountNo, + accountBalance = it.accountBalance, productId = it.productId?.toLong(), productName = it.productName, depositType = GetClientsSavingsAccountsDepositType( diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsSavingsAccounts.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsSavingsAccounts.kt index 86a2d977e18..c7913858e9d 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsSavingsAccounts.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsSavingsAccounts.kt @@ -29,6 +29,8 @@ data class GetClientsSavingsAccounts( val accountNo: String? = null, + val accountBalance: Double? = null, + val currency: GetClientsSavingsAccountsCurrency? = null, val depositType: GetClientsSavingsAccountsDepositType? = null, diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosActionsListingCardComponent.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosActionsListingCardComponent.kt index f267346b297..dfb200d4eac 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosActionsListingCardComponent.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosActionsListingCardComponent.kt @@ -27,7 +27,6 @@ import androidclient.core.ui.generated.resources.core_ui_original_loan import androidclient.core.ui.generated.resources.core_ui_outstanding import androidclient.core.ui.generated.resources.core_ui_paid import androidclient.core.ui.generated.resources.core_ui_quantity -import androidclient.core.ui.generated.resources.core_ui_savings_product import androidclient.core.ui.generated.resources.core_ui_status import androidclient.core.ui.generated.resources.core_ui_total_collateral_value import androidclient.core.ui.generated.resources.core_ui_total_value @@ -377,6 +376,7 @@ fun MifosActionsLoanListingComponent( fun MifosActionsSavingsListingComponent( accountNo: String, savingsProduct: String, + savingsProductName: String, lastActive: String, balance: String, menuList: List, @@ -391,16 +391,15 @@ fun MifosActionsSavingsListingComponent( Column( modifier = Modifier.padding(DesignToken.padding.large), ) { - MifosListingRowItem( - key = stringResource(Res.string.core_ui_account_no), - value = accountNo, + MifosListingRowItemHeader( + text = accountNo, keyStyle = MifosTypography.titleSmallEmphasized, - valueStyle = MifosTypography.titleSmall, ) + Spacer(Modifier.height(DesignToken.padding.large)) MifosListingRowItem( - key = stringResource(Res.string.core_ui_savings_product), - value = savingsProduct, + key = savingsProduct, + value = savingsProductName, ) Spacer(Modifier.height(DesignToken.padding.medium)) Column( @@ -425,6 +424,7 @@ fun MifosActionsSavingsListingComponent( bottomStart = DesignToken.padding.medium, bottomEnd = DesignToken.padding.medium, ), + color = MaterialTheme.colorScheme.surfaceContainer, ) { Column( modifier = Modifier.padding( @@ -627,7 +627,8 @@ fun PreviewMifosActionsSavingsListingComponent() { MaterialTheme { MifosActionsSavingsListingComponent( accountNo = "SV9876", - savingsProduct = "Regular Savings", + savingsProduct = "Savings Product", + savingsProductName = "Wallet", lastActive = "2025-08-15", balance = "$1200", menuList = listOf( diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosListingComponent.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosListingComponent.kt index 0c6bdfa694f..377fca3ee39 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosListingComponent.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosListingComponent.kt @@ -62,6 +62,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.AppColors @@ -118,10 +119,22 @@ fun MifosListingRowItem( MifosListingRowItem( keyContent = { if (key.isNotBlank()) { - Text(text = "$key:", style = keyStyle) + Text( + text = "$key:", + style = keyStyle, + maxLines = 1, + overflow = TextOverflow.Clip, + ) } }, - valueContent = { Text(text = value, style = valueStyle.copy(color = valueColor)) }, + valueContent = { + Text( + text = value, + style = valueStyle.copy(color = valueColor), + overflow = TextOverflow.Clip, + maxLines = 1, + ) + }, ) } diff --git a/feature/client/src/commonMain/composeResources/values/strings.xml b/feature/client/src/commonMain/composeResources/values/strings.xml index 6ca16258a49..6982cf49dfa 100644 --- a/feature/client/src/commonMain/composeResources/values/strings.xml +++ b/feature/client/src/commonMain/composeResources/values/strings.xml @@ -223,6 +223,13 @@ Notes Check or update note accounts + Click Here To View Filled State. + + + Savings Products + Fixed Deposit Products + Recurring Deposit Products + Shares Products Performance History @@ -252,6 +259,8 @@ Collateral Data Check or update the account notes + + Recurring Deposit Accounts Gender diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralScreen.kt index ca1178e0574..e6affe178f7 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralScreen.kt @@ -18,6 +18,7 @@ import androidclient.feature.client.generated.resources.client_performance_histo import androidclient.feature.client.generated.resources.client_profile_general_header_actions import androidclient.feature.client.generated.resources.client_profile_general_header_linked_accounts import androidclient.feature.client.generated.resources.client_profile_general_header_performance_history +import androidclient.feature.client.generated.resources.client_savings_not_avilable import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -239,28 +240,38 @@ fun PerformanceHistoryCard(state: ClientProfileGeneralState) { val performanceHistory = state.performanceHistory PerformanceHistoryRows( stringResource(Res.string.client_performance_history_loan_cycle_count_label), - "${performanceHistory.loanCyclesCount}", + performanceHistory.loanCyclesCount?.toString() + ?: stringResource(Res.string.client_savings_not_avilable), ) PerformanceHistoryRows( stringResource(Res.string.client_performance_history_active_loans_count_label), - "${performanceHistory.activeLoans}", + performanceHistory.activeLoans?.toString() + ?: stringResource(Res.string.client_savings_not_avilable), ) PerformanceHistoryRows( stringResource(Res.string.client_performance_history_last_loan_amount_label), - "${state.currency} ${performanceHistory.lastLoanAmount}", + + performanceHistory.lastLoanAmount?.let { + state.currency + " " + it.toString() + } + ?: stringResource(Res.string.client_savings_not_avilable), ) PerformanceHistoryRows( stringResource(Res.string.client_performance_history_active_savings_label), - "${performanceHistory.activeSavingsCount}", + performanceHistory.activeSavingsCount?.toString() + ?: stringResource(Res.string.client_savings_not_avilable), ) PerformanceHistoryRows( stringResource(Res.string.client_performance_history_total_savings_label), - "${state.currency} ${performanceHistory.lastLoanAmount}", + performanceHistory.totalSaving?.let { + state.currency + " " + it.toString() + } + ?: stringResource(Res.string.client_savings_not_avilable), ) } } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralViewmodel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralViewmodel.kt index a4d0c5d27a4..1d5926b9bd1 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralViewmodel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralViewmodel.kt @@ -17,6 +17,7 @@ import com.mifos.core.data.util.NetworkMonitor import com.mifos.core.domain.useCases.GetClientDetailsUseCase import com.mifos.core.ui.util.BaseViewModel import com.mifos.feature.client.clientGeneral.ClientProfileGeneralEvent.OnActionClick +import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity import com.mifos.room.entities.client.ClientEntity import com.mifos.room.entities.zipmodels.ClientAndClientAccounts import kotlinx.coroutines.flow.update @@ -58,11 +59,16 @@ internal class ClientProfileGeneralViewmodel( val activeSavingsCount = savingAccounts?.count { it.status?.active == true } ?: 0 val totalSaving = - savingAccounts?.filter { it.status?.active == true }?.sumOf { it.accountBalance ?: 0.0 } - ?: 0.0 + savingAccounts?.filter { + it.status?.active == true && + it.depositType?.serverType != SavingAccountDepositTypeEntity.ServerTypes.RECURRING && + it.status?.closed == false + }?.sumOf { + it.accountBalance ?: 0.0 + } // TODO: No function yet created for calculating this value. - val lastLoanAmount = 0.0 + val lastLoanAmount = null return ClientProfileGeneralState.PerformanceHistory( loanCyclesCount = loanCyclesCount, @@ -148,11 +154,11 @@ data class ClientProfileGeneralState( } data class PerformanceHistory( - var loanCyclesCount: Int = 0, - var activeLoans: Int = 0, - var lastLoanAmount: Double = 0.0, - var activeSavingsCount: Int = 0, - var totalSaving: Double = 0.0, + var loanCyclesCount: Int? = null, + var activeLoans: Int? = null, + var lastLoanAmount: Double? = null, + var activeSavingsCount: Int? = null, + var totalSaving: Double? = null, ) } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt index f6b3078266c..1c92f262204 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt @@ -30,6 +30,7 @@ import com.mifos.feature.client.clientTransfer.ClientTransferViewModel import com.mifos.feature.client.clientUpdateDefaultAccount.UpdateDefaultAccountViewModel import com.mifos.feature.client.clientsList.ClientListViewModel import com.mifos.feature.client.createNewClient.CreateNewClientViewModel +import com.mifos.feature.client.recurringDepositAccount.RecurringDepositAccountViewModel import com.mifos.feature.client.savingsAccounts.SavingsAccountsViewModel import com.mifos.feature.client.syncClientDialog.SyncClientsDialogViewModel import org.koin.core.module.dsl.viewModelOf @@ -56,6 +57,7 @@ val ClientModule = module { viewModelOf(::UpdateDefaultAccountViewModel) viewModelOf(::ClientClosureViewModel) viewModelOf(::SavingsAccountsViewModel) + viewModelOf(::RecurringDepositAccountViewModel) viewModelOf(::ClientCollateralViewModel) viewModelOf(::ClientIdentitiesListViewModel) viewModelOf(::ClientApplyNewApplicationsViewModel) diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt index 7be12a9bbeb..4581cc4ea44 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt @@ -51,6 +51,8 @@ import com.mifos.feature.client.clientUpdateDefaultAccount.navigateToUpdateDefau import com.mifos.feature.client.clientUpdateDefaultAccount.updateDefaultAccountDestination import com.mifos.feature.client.clientsList.ClientListScreen import com.mifos.feature.client.createNewClient.CreateNewClientScreen +import com.mifos.feature.client.recurringDepositAccount.clientRecurringDepositAccountDestination +import com.mifos.feature.client.recurringDepositAccount.navigateToRecurringDepositAccountRoute import com.mifos.feature.client.savingsAccounts.navigateToClientSavingsAccountsRoute import com.mifos.feature.client.savingsAccounts.savingsAccountsDestination import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity @@ -139,6 +141,15 @@ fun NavGraphBuilder.clientNavGraph( savingAccounts = { clientId -> navController.navigateToClientSavingsAccountsRoute(clientId) }, + recurringDepositAccounts = { clientId -> + navController.navigateToRecurringDepositAccountRoute(clientId) + }, + ) + + clientRecurringDepositAccountDestination( + navigateBack = navController::popBackStack, + {}, + {}, ) clientProfileDetailsDestination( diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountRoute.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountRoute.kt new file mode 100644 index 00000000000..b2caf8bdf7e --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountRoute.kt @@ -0,0 +1,44 @@ +/* + * 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.client.recurringDepositAccount + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable + +@Serializable +data class RecurringDepositAccountRoute( + val clientId: Int = -1, +) + +fun NavGraphBuilder.clientRecurringDepositAccountDestination( + navigateBack: () -> Unit, + onApproveAccount: (String) -> Unit, + onViewAccount: (String) -> Unit, +) { + composable { + RecurringDepositAccountScreen( + navigateBack = navigateBack, + onApproveAccount = { + onApproveAccount(it) + }, + onViewAccount = { + onViewAccount(it) + }, + ) + } +} + +fun NavController.navigateToRecurringDepositAccountRoute( + clientId: Int, +) { + this.navigate(RecurringDepositAccountRoute(clientId = clientId)) +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountScreen.kt new file mode 100644 index 00000000000..e170e0c4a78 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountScreen.kt @@ -0,0 +1,270 @@ +/* + * 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.client.recurringDepositAccount + +import androidclient.feature.client.generated.resources.Res +import androidclient.feature.client.generated.resources.client_empty_card_message +import androidclient.feature.client.generated.resources.client_product_recurring_deposit_account +import androidclient.feature.client.generated.resources.client_profile_recurring_deposit_account_title +import androidclient.feature.client.generated.resources.client_savings_item +import androidclient.feature.client.generated.resources.client_savings_not_avilable +import androidclient.feature.client.generated.resources.client_savings_pending_approval +import androidclient.feature.client.generated.resources.feature_client_dialog_action_ok +import androidclient.feature.client.generated.resources.filter +import androidclient.feature.client.generated.resources.search +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.common.utils.DateHelper +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTypography +import com.mifos.core.ui.components.Actions +import com.mifos.core.ui.components.MifosActionsSavingsListingComponent +import com.mifos.core.ui.components.MifosEmptyCard +import com.mifos.core.ui.components.MifosSearchBar +import com.mifos.core.ui.util.EventsEffect +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun RecurringDepositAccountScreen( + navigateBack: () -> Unit, + onApproveAccount: (String) -> Unit, + onViewAccount: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: RecurringDepositAccountViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + is RecurringDepositAccountEvent.OnApproveAccount -> { + onApproveAccount(event.accountNumber) + } + + RecurringDepositAccountEvent.OnNavigateBack -> navigateBack() + is RecurringDepositAccountEvent.OnViewAccount -> { + onViewAccount(event.accountNumber) + } + } + } + + RecurringDepositAccountDialog( + state, + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + ) + + RecurringDepositAccountScaffold( + state = state, + modifier = modifier, + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RecurringDepositAccountDialog( + state: RecurringDepositAccountState, + onAction: (RecurringDepositAccountAction) -> Unit, +) { + when (state.dialogState) { + is RecurringDepositAccountState.DialogState.Error -> { + AlertDialog( + title = { Text("Error") }, + text = { Text(text = state.dialogState.message) }, + confirmButton = { + TextButton( + onClick = { + onAction(RecurringDepositAccountAction.CloseDialog) + }, + ) { + Text(stringResource(Res.string.feature_client_dialog_action_ok)) + } + }, + onDismissRequest = {}, + ) + } + + RecurringDepositAccountState.DialogState.Loading -> MifosCircularProgress() + + else -> null + } +} + +@Composable +internal fun RecurringDepositAccountScaffold( + state: RecurringDepositAccountState, + modifier: Modifier = Modifier, + onAction: (RecurringDepositAccountAction) -> Unit, +) { + MifosScaffold( + onBackPressed = { + onAction(RecurringDepositAccountAction.NavigateBack) + }, + modifier = modifier, + title = "", + ) { paddingValues -> + + Column( + Modifier.fillMaxSize() + .padding(paddingValues) + .padding( + vertical = DesignToken.padding.extraLarge, + horizontal = DesignToken.padding.large, + ), + ) { + val notAvailableText = stringResource(Res.string.client_savings_not_avilable) + RecurringDepositAccountHeader( + state.recurringDepositAccounts.size.toString(), + onToggleSearch = { + onAction(RecurringDepositAccountAction.ToggleSearch) + }, + onToggleFilter = { + onAction(RecurringDepositAccountAction.ToggleFilter) + }, + ) + + // todo implement search bar functionality + if (state.isSearchBarActive) { + MifosSearchBar( + query = state.searchText, + onQueryChange = { + onAction(RecurringDepositAccountAction.UpdateSearch(it)) + }, + onSearchClick = { + onAction(RecurringDepositAccountAction.Search(it)) + }, + onBackClick = { + onAction(RecurringDepositAccountAction.ToggleSearch) + }, + ) + } + + Spacer(modifier = Modifier.height(DesignToken.padding.largeIncreasedExtra)) + + if (state.recurringDepositAccounts.isEmpty()) { + MifosEmptyCard(msg = stringResource(Res.string.client_empty_card_message)) + } else { + LazyColumn { + items(state.recurringDepositAccounts) { recurringDeposit -> + MifosActionsSavingsListingComponent( + accountNo = recurringDeposit.accountNo ?: notAvailableText, + savingsProduct = stringResource(Res.string.client_product_recurring_deposit_account), + savingsProductName = recurringDeposit.shortProductName ?: notAvailableText, + lastActive = if (recurringDeposit.status?.submittedAndPendingApproval == true) { + stringResource(Res.string.client_savings_pending_approval) + } else if (recurringDeposit.lastActiveTransactionDate != null) { + DateHelper.getDateAsString(recurringDeposit.lastActiveTransactionDate!!) + } else { + notAvailableText + }, + balance = recurringDeposit.accountBalance?.toString() ?: notAvailableText, + menuList = if (recurringDeposit.status?.submittedAndPendingApproval == true) { + listOf( + Actions.ViewAccount, + Actions.ApproveAccount, + ) + } else { + listOf( + Actions.ViewAccount, + ) + }, + ) { actions -> + when (actions) { + Actions.ViewAccount -> { + onAction( + RecurringDepositAccountAction.ViewAccount( + recurringDeposit.accountNo ?: "", + ), + ) + } + Actions.ApproveAccount -> { + RecurringDepositAccountAction.ApproveAccount( + recurringDeposit.accountNo ?: "", + ) + } + else -> null + } + } + + Spacer(modifier = Modifier.height(DesignToken.spacing.small)) + } + } + } + } + } +} + +@Composable +private fun RecurringDepositAccountHeader( + totalItem: String, + onToggleFilter: () -> Unit, + modifier: Modifier = Modifier, + onToggleSearch: () -> Unit, +) { + Row( + modifier = modifier.fillMaxWidth() + .wrapContentHeight(), + ) { + Column { + Text( + text = stringResource(Res.string.client_profile_recurring_deposit_account_title), + style = MifosTypography.titleMedium, + ) + + Text( + text = totalItem + " " + stringResource(Res.string.client_savings_item), + style = MifosTypography.labelMedium, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource(Res.drawable.search), + contentDescription = null, + modifier = Modifier.clickable { + onToggleSearch.invoke() + }, + ) + + Spacer(modifier = Modifier.width(DesignToken.spacing.largeIncreased)) + + Icon( + painter = painterResource(Res.drawable.filter), + contentDescription = null, + modifier = Modifier.clickable { + onToggleFilter.invoke() + }, + ) + } +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountViewModel.kt new file mode 100644 index 00000000000..ecfbb5d8229 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/recurringDepositAccount/RecurringDepositAccountViewModel.kt @@ -0,0 +1,204 @@ +/* + * 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.client.recurringDepositAccount + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.domain.useCases.GetClientDetailsUseCase +import com.mifos.core.ui.util.BaseViewModel +import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity +import com.mifos.room.entities.accounts.savings.SavingsAccountEntity +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class RecurringDepositAccountViewModel( + savedStateHandle: SavedStateHandle, + private val networkMonitor: NetworkMonitor, + private val getClientDetailsUseCase: GetClientDetailsUseCase, +) : BaseViewModel< + RecurringDepositAccountState, + RecurringDepositAccountEvent, + RecurringDepositAccountAction, + > + (initialState = RecurringDepositAccountState()) { + + val route = savedStateHandle.toRoute() + + init { + checkNetworkAndGetLoanAccounts() + } + + override fun handleAction(action: RecurringDepositAccountAction) { + when (action) { + RecurringDepositAccountAction.CloseDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + is RecurringDepositAccountAction.NavigateBack -> { + sendEvent(RecurringDepositAccountEvent.OnNavigateBack) + } + + is RecurringDepositAccountAction.Refresh -> { + checkNetworkAndGetLoanAccounts() + } + + is RecurringDepositAccountAction.Search -> { + checkNetworkAndGetLoanAccounts() + } + + is RecurringDepositAccountAction.ToggleFilter -> { + mutableStateFlow.update { + it.copy( + isFilterDialogOpen = true, + ) + } + } + + is RecurringDepositAccountAction.ToggleSearch -> { + mutableStateFlow.update { + it.copy( + isSearchBarActive = true, + ) + } + } + + is RecurringDepositAccountAction.UpdateSearch -> { + mutableStateFlow.update { + it.copy( + searchText = action.query, + ) + } + } + + is RecurringDepositAccountAction.ViewAccount -> { + sendEvent( + RecurringDepositAccountEvent.OnViewAccount(action.accountNumber), + ) + } + + is RecurringDepositAccountAction.ApproveAccount -> { + sendEvent( + RecurringDepositAccountEvent.OnApproveAccount(action.accountNumber), + ) + } + } + } + + private fun checkNetworkAndGetLoanAccounts() { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + when (isConnected) { + true -> getRecurringDepositAccounts() + false -> { + mutableStateFlow.update { + it.copy( + dialogState = RecurringDepositAccountState + .DialogState.Error("No internet connection, Try Again"), + ) + } + } + } + } + } + } + + private fun getRecurringDepositAccounts() { + viewModelScope.launch { + getClientDetailsUseCase.invoke(route.clientId).collect { result -> + when (result) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = RecurringDepositAccountState.DialogState.Error( + result.message, + ), + ) + } + } + + is DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = RecurringDepositAccountState.DialogState.Loading) + } + } + + is DataState.Success -> { + val recurringDepositAccount = + result.data.clientAccounts?.savingsAccounts?.let { + it.filter { accountEntity -> + accountEntity.depositType?.serverType == SavingAccountDepositTypeEntity.ServerTypes.RECURRING && accountEntity.status?.closed == false + }.apply { + // Todo modify search accordingly + searchRecurringDepositAccounts(state.searchText, this) + } + } ?: emptyList() + + mutableStateFlow.update { + it.copy( + dialogState = null, + clientId = route.clientId, + recurringDepositAccounts = recurringDepositAccount, + ) + } + } + } + } + } + } + + private fun searchRecurringDepositAccounts( + query: String, + recurringDepositAccounts: List, + ): List { + if (query.isNotBlank()) { + return recurringDepositAccounts.filter { accountEntity -> + accountEntity.accountNo.toString().contains(state.searchText.trim()) + } + } + return recurringDepositAccounts + } +} + +data class RecurringDepositAccountState( + val clientId: Int = -1, + val recurringDepositAccounts: List = emptyList(), + val searchText: String = "", + val dialogState: DialogState? = null, + val isSearchBarActive: Boolean = false, + val isFilterDialogOpen: Boolean = false, +) { + sealed interface DialogState { + data class Error(val message: String) : DialogState + data object Loading : DialogState + } +} + +sealed class RecurringDepositAccountAction { + data object NavigateBack : RecurringDepositAccountAction() + data class ViewAccount(val accountNumber: String) : RecurringDepositAccountAction() + data class ApproveAccount(val accountNumber: String) : RecurringDepositAccountAction() + data object Refresh : RecurringDepositAccountAction() + data object ToggleFilter : RecurringDepositAccountAction() + data object ToggleSearch : RecurringDepositAccountAction() + data class Search(val query: String) : RecurringDepositAccountAction() + data class UpdateSearch(val query: String) : RecurringDepositAccountAction() + data object CloseDialog : RecurringDepositAccountAction() +} + +sealed class RecurringDepositAccountEvent { + data object OnNavigateBack : RecurringDepositAccountEvent() + data class OnViewAccount(val accountNumber: String) : RecurringDepositAccountEvent() + data class OnApproveAccount(val accountNumber: String) : RecurringDepositAccountEvent() +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccounts.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccounts.kt index 230ee157791..12c5ce59acb 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccounts.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccounts.kt @@ -10,6 +10,7 @@ package com.mifos.feature.client.savingsAccounts import androidclient.feature.client.generated.resources.Res +import androidclient.feature.client.generated.resources.client_product_saving_account import androidclient.feature.client.generated.resources.client_savings_item import androidclient.feature.client.generated.resources.client_savings_not_avilable import androidclient.feature.client.generated.resources.client_savings_pending_approval @@ -26,7 +27,6 @@ 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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.AlertDialog @@ -39,7 +39,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -123,7 +122,8 @@ fun SavingsAccountsScreen( items(state.savingsAccounts) { savings -> MifosActionsSavingsListingComponent( accountNo = savings.accountNo.toString(), - savingsProduct = savings.productName.toString(), + savingsProduct = stringResource(Res.string.client_product_saving_account), + savingsProductName = savings.productName.toString(), // todo modify with currency symbol when not getting null from api, currently getting null balance = if (savings.accountBalance != null) { savings.accountBalance.toString() @@ -166,7 +166,7 @@ fun SavingsAccountsScreen( }, ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(DesignToken.spacing.small)) } } } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccountsViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccountsViewModel.kt index c6f1097e925..f69ce016020 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccountsViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/savingsAccounts/SavingsAccountsViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.mifos.core.data.repository.ClientDetailsRepository import com.mifos.core.ui.util.BaseViewModel +import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -83,6 +84,10 @@ internal class SavingsAccountsViewModel( // Todo modify search accordingly val savingsAccounts = repository.getClientAccounts(route.clientId) .savingsAccounts + .filter { accountEntity -> + accountEntity.depositType?.serverType == SavingAccountDepositTypeEntity.ServerTypes.SAVINGS && + accountEntity.status?.closed == false + } .filter { it.accountNo?.contains(state.searchText.trim()) == true } mutableStateFlow.update {