diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateSavingsAccountUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateSavingsAccountUseCase.kt index 0814906894..02c1800fed 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateSavingsAccountUseCase.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateSavingsAccountUseCase.kt @@ -23,6 +23,6 @@ class CreateSavingsAccountUseCase( private val repository: SavingsAccountRepository, ) { - operator fun invoke(savingsPayload: SavingsPayload?): Flow> = + operator fun invoke(savingsPayload: SavingsPayload?): Flow> = repository.createSavingsAccount(savingsPayload) } diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/payloads/SavingsPayload.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/payloads/SavingsPayload.kt index 3873743934..8f905af875 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/payloads/SavingsPayload.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/payloads/SavingsPayload.kt @@ -32,6 +32,11 @@ class SavingsPayload { var allowOverdraft: Boolean? = null var enforceMinRequiredBalance: Boolean? = null var minRequiredOpeningBalance: String? = null + var minRequiredBalance: String? = null + var lockinPeriodFrequency: Int? = null + var lockinPeriodFrequencyType: Int? = null + + var charges: List? = null var nominalAnnualInterestRateOverdraft: String? = null var overdraftLimit: String? = null var minOverdraftForInterestCalculation: String? = null diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerSavings.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerSavings.kt index a998756ed1..338db67e8a 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerSavings.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerSavings.kt @@ -12,6 +12,7 @@ package com.mifos.core.network.datamanager import com.mifos.core.datastore.UserPreferencesRepository import com.mifos.core.model.objects.account.loan.SavingsApproval import com.mifos.core.model.objects.account.saving.SavingsAccountTransactionResponse +import com.mifos.core.model.objects.error.MifosError import com.mifos.core.model.objects.organisations.ProductSavings import com.mifos.core.model.objects.payloads.SavingsPayload import com.mifos.core.network.BaseApiManager @@ -22,11 +23,14 @@ import com.mifos.room.entities.client.Savings import com.mifos.room.entities.templates.savings.SavingProductsTemplate import com.mifos.room.entities.templates.savings.SavingsAccountTransactionTemplateEntity import com.mifos.room.helper.SavingsDaoHelper +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json /** * Created by Rajan Maurya on 17/08/16. @@ -285,7 +289,24 @@ class DataManagerSavings( get() = mBaseApiManager.savingsService.allSavingsAccounts() fun createSavingsAccount(savingsPayload: SavingsPayload?): Flow { - return mBaseApiManager.savingsService.createSavingsAccount(savingsPayload) + return mBaseApiManager.savingsService.createSavingsAccount(savingsPayload).map { response -> + val responseText = response.bodyAsText() + val json = Json { ignoreUnknownKeys = true } + + if (!response.status.isSuccess()) { + val errorMessage = try { + val errorResponse = json.decodeFromString(responseText) + errorResponse.errors.firstOrNull()?.defaultUserMessage + ?: errorResponse.defaultUserMessage + ?: "HTTP ${response.status.value} ${response.status.description}" + } catch (e: Exception) { + "HTTP ${response.status.value} ${response.status.description}" + } + throw IllegalStateException(errorMessage) + } + + json.decodeFromString(responseText) + } } val getSavingsAccountTemplate: Flow diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/services/SavingsAccountService.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/services/SavingsAccountService.kt index e46c4cdae5..07b3ba378a 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/services/SavingsAccountService.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/services/SavingsAccountService.kt @@ -17,7 +17,6 @@ import com.mifos.core.network.GenericResponse import com.mifos.room.basemodel.APIEndPoint import com.mifos.room.entities.accounts.savings.SavingsAccountTransactionRequestEntity import com.mifos.room.entities.accounts.savings.SavingsAccountWithAssociationsEntity -import com.mifos.room.entities.client.Savings import com.mifos.room.entities.templates.savings.SavingProductsTemplate import com.mifos.room.entities.templates.savings.SavingsAccountTransactionTemplateEntity import de.jensklingenberg.ktorfit.http.Body @@ -25,6 +24,7 @@ import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.Path import de.jensklingenberg.ktorfit.http.Query +import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow /** @@ -106,7 +106,7 @@ interface SavingsAccountService { fun allSavingsAccounts(): Flow> @POST(APIEndPoint.CREATE_SAVINGS_ACCOUNTS) - fun createSavingsAccount(@Body savingsPayload: SavingsPayload?): Flow + fun createSavingsAccount(@Body savingsPayload: SavingsPayload?): Flow @GET(APIEndPoint.CREATE_SAVINGS_PRODUCTS + "/template") fun savingsAccountTemplate(): Flow diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosGeneralCard.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosGeneralCard.kt new file mode 100644 index 0000000000..2b41c3d067 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosGeneralCard.kt @@ -0,0 +1,113 @@ +/* + * 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.ui.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTypography +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun MifosGeneralCardComponentOutline( + modifier: Modifier = Modifier, + borderCorner: Dp = DesignToken.sizes.iconMiny, + content: @Composable () -> Unit, +) { + Box( + modifier = modifier + .border( + width = 1.dp, + shape = RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + bottomStart = borderCorner, + bottomEnd = borderCorner, + ), + color = MaterialTheme.colorScheme.secondaryContainer, + ), + ) { + content() + } +} + +@Composable +fun MifosGeneralRowItem( + keyContent: @Composable () -> Unit, + valueContent: @Composable () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterStart, + ) { keyContent() } + + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterEnd, + ) { valueContent() } + } +} + +@Composable +fun MifosGeneralCard( + modifier: Modifier = Modifier, + contentMap: Map, + separator: String = " : ", +) { + MifosGeneralCardComponentOutline { + Column( + modifier = modifier.padding(DesignToken.padding.large), + ) { + contentMap.entries.forEachIndexed { index, map -> + MifosGeneralRowItem( + keyContent = { Text(text = map.key + separator, style = MifosTypography.labelMediumEmphasized) }, + valueContent = { Text(text = map.value, style = MifosTypography.labelMediumEmphasized) }, + ) + if (index < contentMap.size - 1) { + Spacer(modifier = Modifier.height(DesignToken.padding.small)) + } + } + } + } +} + +@Preview +@Composable +fun MifosPreviewGeneralCard() { + MaterialTheme { + MifosGeneralCard( + contentMap = mapOf( + "title" to "answer", + "title1" to "ans1", + "title2" to "ans2", + "title3" to "ans3", + "title4" to "ans4", + ), + ) + } +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralNavigation.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralNavigation.kt index 3fe0f6b75d..12d80ba05f 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralNavigation.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientGeneral/ClientProfileGeneralNavigation.kt @@ -12,6 +12,8 @@ package com.mifos.feature.client.clientGeneral import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.mifos.feature.client.clientProfile.ClientProfileRoute +import com.mifos.feature.client.navigation.ClientListScreenRoute import kotlinx.serialization.Serializable @Serializable @@ -51,3 +53,12 @@ fun NavController.navigateToClientProfileGeneralRoute(id: Int) { ClientProfileGeneralRoute(id = id), ) } + +fun NavController.navigateToClientProfileGeneralRouteOnStatus(id: Int) { + popBackStack(0, true) + navigate(ClientListScreenRoute) + navigate(ClientProfileRoute(id)) + navigate(ClientProfileGeneralRoute(id = id)) { + launchSingleTop = true + } +} 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 8e04acf5fd..18479a2470 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 @@ -45,6 +45,7 @@ import com.mifos.feature.client.clientEditProfile.clientEditProfileDestination import com.mifos.feature.client.clientEditProfile.navigateToClientProfileEditProfileRoute import com.mifos.feature.client.clientGeneral.clientProfileGeneralDestination import com.mifos.feature.client.clientGeneral.navigateToClientProfileGeneralRoute +import com.mifos.feature.client.clientGeneral.navigateToClientProfileGeneralRouteOnStatus import com.mifos.feature.client.clientIdentifiersAddUpdate.clientIdentifiersAddUpdateDestination import com.mifos.feature.client.clientIdentifiersAddUpdate.onNavigateToClientIdentifiersAddUpdateScreen import com.mifos.feature.client.clientIdentifiersList.clientIdentifiersListDestination @@ -339,6 +340,7 @@ fun NavGraphBuilder.clientNavGraph( onBackPressed = navController::popBackStack, loadMoreSavingsAccountInfo = navController::navigateToDataTable, loadDocuments = navController::navigateToDocumentListScreen, + onFinish = navController::navigateToClientProfileGeneralRouteOnStatus, ) shareAccountDestination() diff --git a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml index 52f520e696..2ff5d6f354 100644 --- a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml +++ b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml @@ -13,7 +13,7 @@ Savings Account Summary Documents Client Name - Product Name + Product Name* Account Balance Total Deposits Total Withdrawals @@ -28,7 +28,7 @@ Running Balance Saving Account Id Account Number - Currency + Currency* Approve Savings Activate Savings Savings Account Closed @@ -67,7 +67,7 @@ Back Add Savings Account The Savings Account has been submitted forApproval - Field Officer + Field Officer* External ID Submitted On Nominal annual interest @@ -139,6 +139,17 @@ Active Edit Charge + Yes + No + Continue + Success + Failed + + New Savings Account Application Submitted Successfully + Failed to apply for new savings account with the provided details + + %1$d active charges + diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt index 67cfa1f966..e14bd6d5f1 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/navigation/SavingsNavigation.kt @@ -28,6 +28,7 @@ import kotlinx.serialization.Serializable fun NavGraphBuilder.savingsDestination( navController: NavController, onBackPressed: () -> Unit, + onFinish: (id: Int) -> Unit, loadMoreSavingsAccountInfo: (String, Int) -> Unit, loadDocuments: (Int, String) -> Unit, ) { @@ -85,7 +86,7 @@ fun NavGraphBuilder.savingsDestination( savingsAccountDestination( navController = navController, onNavigateBack = onBackPressed, - onFinish = onBackPressed, + onFinish = onFinish, ) } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccount/SavingAccountViewModel.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccount/SavingAccountViewModel.kt index 184b818661..60013e9e93 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccount/SavingAccountViewModel.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccount/SavingAccountViewModel.kt @@ -22,6 +22,7 @@ import com.mifos.core.domain.useCases.GetGroupSavingsAccountTemplateByProductUse import com.mifos.core.domain.useCases.LoadSavingsAccountsAndTemplateUseCase import com.mifos.core.model.objects.payloads.SavingsPayload import com.mifos.feature.savings.navigation.SavingsAccountRoute +import com.mifos.room.entities.client.Savings import com.mifos.room.entities.templates.savings.SavingProductsTemplate import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -128,11 +129,13 @@ class SavingAccountViewModel( _savingAccountUiState.value = SavingAccountUiState.ShowProgress - is DataState.Success -> + is DataState.Success -> { _savingAccountUiState.value = SavingAccountUiState.ShowSavingsAccountCreatedSuccessfully( - dataState.data, + Savings(), +// dataState.data, ) + } } } } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt index e91ecd128c..6fd5aeff70 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountRoute.kt @@ -22,7 +22,7 @@ data class SavingsAccountRoute( fun NavGraphBuilder.savingsAccountDestination( navController: NavController, onNavigateBack: () -> Unit, - onFinish: () -> Unit, + onFinish: (id: Int) -> Unit, ) { composable { SavingsAccountScreen( diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt index 687914160e..08abbfa5f2 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt @@ -12,8 +12,12 @@ package com.mifos.feature.savings.savingsAccountv2 import androidclient.feature.savings.generated.resources.Res import androidclient.feature.savings.generated.resources.feature_savings_back import androidclient.feature.savings.generated.resources.feature_savings_cancel +import androidclient.feature.savings.generated.resources.feature_savings_continue import androidclient.feature.savings.generated.resources.feature_savings_create_savings_account import androidclient.feature.savings.generated.resources.feature_savings_error_not_connected_internet +import androidclient.feature.savings.generated.resources.feature_savings_failed +import androidclient.feature.savings.generated.resources.feature_savings_retry +import androidclient.feature.savings.generated.resources.feature_savings_success import androidclient.feature.savings.generated.resources.step_charges import androidclient.feature.savings.generated.resources.step_charges_add import androidclient.feature.savings.generated.resources.step_charges_add_new @@ -50,8 +54,10 @@ import com.mifos.core.ui.components.MifosActionsChargeListingComponent import com.mifos.core.ui.components.MifosBreadcrumbNavBar import com.mifos.core.ui.components.MifosProgressIndicator import com.mifos.core.ui.components.MifosProgressIndicatorOverlay +import com.mifos.core.ui.components.MifosStatusDialog import com.mifos.core.ui.components.MifosStepper import com.mifos.core.ui.components.MifosTwoButtonRow +import com.mifos.core.ui.components.ResultStatus import com.mifos.core.ui.components.Step import com.mifos.core.ui.util.EventsEffect import com.mifos.core.ui.util.TextFieldsValidator.doubleNumberValidator @@ -68,7 +74,7 @@ import kotlin.time.ExperimentalTime internal fun SavingsAccountScreen( navController: NavController, onNavigateBack: () -> Unit, - onFinish: () -> Unit, + onFinish: (id: Int) -> Unit, modifier: Modifier = Modifier, viewModel: SavingsAccountViewModel = koinViewModel(), ) { @@ -77,7 +83,7 @@ internal fun SavingsAccountScreen( EventsEffect(viewModel.eventFlow) { event -> when (event) { SavingsAccountEvent.NavigateBack -> onNavigateBack() - SavingsAccountEvent.Finish -> onFinish() + SavingsAccountEvent.Finish -> onFinish(state.clientId) } } @@ -121,9 +127,10 @@ private fun SavingsAccountScaffold( ) }, Step(stringResource(Res.string.step_preview)) { - PreviewPage { - onAction(SavingsAccountAction.NextStep) - } + PreviewPage( + state = state, + onAction = onAction, + ) }, ) @@ -159,6 +166,19 @@ private fun SavingsAccountScaffold( onclick = { onAction(SavingsAccountAction.Retry) }, ) } + + is SavingsAccountState.ScreenState.ShowStatusDialog -> { + MifosStatusDialog( + status = state.screenState.status, + btnText = if (state.screenState.status == ResultStatus.SUCCESS) stringResource(Res.string.feature_savings_continue) else stringResource(Res.string.feature_savings_retry), + onConfirm = { if (state.screenState.status == ResultStatus.SUCCESS) onAction(SavingsAccountAction.Finish) else onAction(SavingsAccountAction.Retry) }, + successTitle = stringResource(Res.string.feature_savings_success), + successMessage = state.screenState.msg, + failureTitle = stringResource(Res.string.feature_savings_failed), + failureMessage = state.screenState.msg, + modifier = Modifier.fillMaxSize(), + ) + } } if (state.isOverLayLoadingActive) { MifosProgressIndicatorOverlay() diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt index 9236819b48..0cd9ceae2b 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountViewModel.kt @@ -9,14 +9,21 @@ */ package com.mifos.feature.savings.savingsAccountv2 +import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_new_savings_account_submitted_failed +import androidclient.feature.savings.generated.resources.feature_savings_new_savings_account_submitted_success import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.DateHelper import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.domain.useCases.CreateSavingsAccountUseCase import com.mifos.core.domain.useCases.GetClientTemplateUseCase import com.mifos.core.domain.useCases.GetSavingsProductTemplateUseCase +import com.mifos.core.model.objects.payloads.ChargesPayload +import com.mifos.core.model.objects.payloads.SavingsPayload +import com.mifos.core.ui.components.ResultStatus import com.mifos.core.ui.util.BaseViewModel import com.mifos.core.ui.util.TextFieldsValidator import com.mifos.room.entities.templates.clients.ClientsTemplateEntity @@ -27,6 +34,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.getString import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -34,6 +42,7 @@ internal class SavingsAccountViewModel( private val networkMonitor: NetworkMonitor, private val getClientTemplateUseCase: GetClientTemplateUseCase, private val getSavingsProductTemplateUseCase: GetSavingsProductTemplateUseCase, + private val createSavingsAccountUseCase: CreateSavingsAccountUseCase, val savedStateHandle: SavedStateHandle, ) : BaseViewModel( @@ -52,10 +61,18 @@ internal class SavingsAccountViewModel( is SavingsAccountAction.NavigateBack -> sendEvent(SavingsAccountEvent.NavigateBack) is SavingsAccountAction.NextStep -> moveToNextStep() is SavingsAccountAction.PreviousStep -> moveToPreviousStep() - is SavingsAccountAction.Finish -> sendEvent(SavingsAccountEvent.Finish) + is SavingsAccountAction.Finish -> { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.Success, + ) + } + sendEvent(SavingsAccountEvent.Finish) + } is SavingsAccountAction.OnStepChange -> handleStepChange(action) - is SavingsAccountAction.OnSubmissionDatePick -> handleSubmissionDatePick(action) is SavingsAccountAction.Retry -> handleRetry() + + is SavingsAccountAction.OnSubmissionDatePick -> handleSubmissionDatePick(action) is SavingsAccountAction.OnSubmissionDateChange -> handleSubmissionDateChange(action) is SavingsAccountAction.OnDetailsSubmit -> handleOnDetailsSubmit() is SavingsAccountAction.OnExternalIdChange -> handleExternalIdChange(action) @@ -99,6 +116,92 @@ internal class SavingsAccountViewModel( is SavingsAccountAction.DeleteChargeFromSelectedCharges -> handleDeleteCharge(action.index) is SavingsAccountAction.EditChargeDialog -> handleEditChargeDialog(action.index) is SavingsAccountAction.OnChargesAmountChangeError -> handleChargesAmountChangeError(action.error) + is SavingsAccountAction.SubmitSavingsApplication -> handleFinishClick() + } + } + + private fun handleFinishClick() { + submitSavingsApplication(createSavingsPayload()) + } + + private fun createSavingsPayload(): SavingsPayload { + val savingsPayload = SavingsPayload() + savingsPayload.apply { + locale = "en" + dateFormat = "dd-MM-yyyy" + productId = state.savingProductOptions.getOrNull(state.savingsProductSelected)?.id + clientId = state.clientId + fieldOfficerId = state.fieldOfficerOptions.getOrNull(state.fieldOfficerIndex)?.id + submittedOnDate = state.submissionDate + externalId = state.externalId + allowOverdraft = state.isCheckedOverdraftAllowed + enforceMinRequiredBalance = state.isCheckedMinimumBalance + minRequiredOpeningBalance = state.minimumOpeningBalance + minRequiredBalance = state.monthlyMinimumBalance + lockinPeriodFrequency = state.frequency.toIntOrNull() + lockinPeriodFrequencyType = state.savingsProductTemplate?.lockinPeriodFrequencyTypeOptions + ?.getOrNull(state.freqTypeIndex)?.id + .takeIf { state.frequency.toIntOrNull() != null } + charges = state.addedCharges.map { charges -> + ChargesPayload( + chargeId = charges.id, + amount = charges.amount.toString(), + ) + } + interestCompoundingPeriodType = + state.savingsProductTemplate?.interestCompoundingPeriodTypeOptions?.getOrNull(state.interestCompPeriodIndex)?.id + interestCalculationType = + state.savingsProductTemplate?.interestCalculationTypeOptions?.getOrNull(state.interestCalcIndex)?.id + interestCalculationDaysInYearType = + state.savingsProductTemplate?.interestCalculationDaysInYearTypeOptions?.getOrNull(state.daysInYearIndex)?.id + interestPostingPeriodType = + state.savingsProductTemplate?.interestPostingPeriodTypeOptions?.getOrNull(state.interestPostingPeriodIndex)?.id + } + return savingsPayload + } + + private fun submitSavingsApplication(savingsPayload: SavingsPayload) = viewModelScope.launch { + val online = networkMonitor.isOnline.first() + if (online) { + createSavingsAccountUseCase(savingsPayload).collect { result -> + when (result) { + is DataState.Loading -> { + mutableStateFlow.update { + it.copy( + isOverLayLoadingActive = true, + ) + } + } + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + isOverLayLoadingActive = false, + screenState = SavingsAccountState.ScreenState.ShowStatusDialog( + ResultStatus.SUCCESS, + getString(Res.string.feature_savings_new_savings_account_submitted_success), + ), + ) + } + } + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.ShowStatusDialog( + ResultStatus.FAILURE, + msg = result.exception.message ?: getString(Res.string.feature_savings_new_savings_account_submitted_failed), + ), + isOverLayLoadingActive = false, + ) + } + } + } + } + } else { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.NetworkError, + ) + } } } @@ -479,8 +582,6 @@ internal class SavingsAccountViewModel( currentStep = current + 1, ) } - } else { - sendEvent(SavingsAccountEvent.Finish) } } } @@ -544,6 +645,7 @@ constructor( data object Loading : ScreenState data object Success : ScreenState data object NetworkError : ScreenState + data class ShowStatusDialog(val status: ResultStatus, val msg: String = "") : ScreenState } val isDetailsNextEnabled = submissionDate.isNotEmpty() && @@ -567,6 +669,7 @@ sealed interface SavingsAccountAction { data object NextStep : SavingsAccountAction data object PreviousStep : SavingsAccountAction data object Finish : SavingsAccountAction + data object SubmitSavingsApplication : SavingsAccountAction data class OnStepChange(val newIndex: Int) : SavingsAccountAction data class OnSubmissionDateChange(val date: String) : SavingsAccountAction data class OnSubmissionDatePick(val state: Boolean) : SavingsAccountAction @@ -616,7 +719,8 @@ sealed interface SavingsAccountAction { sealed interface Internal : SavingsAccountAction { data class OnReceivingClientTemplate(val clientTemplate: DataState) : Internal - data class OnReceivingSavingsProductTemplate(val savingsProductTemplate: DataState) : Internal + data class OnReceivingSavingsProductTemplate(val savingsProductTemplate: DataState) : + Internal } } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt index 8c6b124c74..ec86b4c93f 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/PreviewPage.kt @@ -10,26 +10,190 @@ package com.mifos.feature.savings.savingsAccountv2.pages import androidclient.feature.savings.generated.resources.Res +import androidclient.feature.savings.generated.resources.feature_savings_back +import androidclient.feature.savings.generated.resources.feature_savings_charges_active_count +import androidclient.feature.savings.generated.resources.feature_savings_currency +import androidclient.feature.savings.generated.resources.feature_savings_days_in_year +import androidclient.feature.savings.generated.resources.feature_savings_external_id +import androidclient.feature.savings.generated.resources.feature_savings_field_officer +import androidclient.feature.savings.generated.resources.feature_savings_interest_calc +import androidclient.feature.savings.generated.resources.feature_savings_interest_comp +import androidclient.feature.savings.generated.resources.feature_savings_interest_p_period +import androidclient.feature.savings.generated.resources.feature_savings_no +import androidclient.feature.savings.generated.resources.feature_savings_product_name +import androidclient.feature.savings.generated.resources.feature_savings_submission_date import androidclient.feature.savings.generated.resources.feature_savings_submit -import androidclient.feature.savings.generated.resources.step_preview +import androidclient.feature.savings.generated.resources.feature_savings_yes +import androidclient.feature.savings.generated.resources.step_charges +import androidclient.feature.savings.generated.resources.step_charges_view +import androidclient.feature.savings.generated.resources.step_details +import androidclient.feature.savings.generated.resources.step_terms +import androidclient.feature.savings.generated.resources.step_terms_apply_withdrawal_fee +import androidclient.feature.savings.generated.resources.step_terms_decimal_places +import androidclient.feature.savings.generated.resources.step_terms_is_allowed_overdraft +import androidclient.feature.savings.generated.resources.step_terms_lock_in_period +import androidclient.feature.savings.generated.resources.step_terms_min_opening_balance +import androidclient.feature.savings.generated.resources.step_terms_minimum_balance import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTypography +import com.mifos.core.ui.components.MifosGeneralCard +import com.mifos.core.ui.components.MifosRowWithTextAndButton +import com.mifos.core.ui.components.MifosTwoButtonRow +import com.mifos.feature.savings.savingsAccountv2.SavingsAccountAction +import com.mifos.feature.savings.savingsAccountv2.SavingsAccountState import org.jetbrains.compose.resources.stringResource +import kotlin.collections.mapOf @Composable -fun PreviewPage(onNext: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(stringResource(Res.string.step_preview)) - Spacer(Modifier.height(8.dp)) - Button(onClick = onNext) { - Text(stringResource(Res.string.feature_savings_submit)) +fun PreviewPage( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, + modifier: Modifier = Modifier, +) { + val previewDetailsMap = mapOf( + stringResource(Res.string.feature_savings_product_name) to + state.savingProductOptions.getOrNull(state.savingsProductSelected)?.name.orEmpty(), + stringResource(Res.string.feature_savings_field_officer) to + state.fieldOfficerOptions.getOrNull(state.fieldOfficerIndex)?.displayName.orEmpty(), + stringResource(Res.string.feature_savings_submission_date) to state.submissionDate, + stringResource(Res.string.feature_savings_external_id) to state.externalId, + ) + + val termsDetailsMap = mapOf( + stringResource(Res.string.feature_savings_currency) to ( + state.savingsProductTemplate?.currencyOptions?.getOrNull( + state.currencyIndex, + )?.name.orEmpty() + ), + stringResource(Res.string.step_terms_decimal_places) to state.decimalPlaces, + stringResource(Res.string.feature_savings_interest_comp) to ( + state.savingsProductTemplate?.interestCompoundingPeriodTypeOptions?.getOrNull( + state.interestCompPeriodIndex, + )?.value.orEmpty() + ), + stringResource(Res.string.feature_savings_interest_p_period) to ( + state.savingsProductTemplate?.interestPostingPeriodTypeOptions?.getOrNull( + state.interestPostingPeriodIndex, + )?.value.orEmpty() + ), + stringResource(Res.string.feature_savings_interest_calc) to ( + state.savingsProductTemplate?.interestCalculationTypeOptions?.getOrNull( + state.interestCalcIndex, + )?.value.orEmpty() + ), + + stringResource(Res.string.feature_savings_days_in_year) to ( + state.savingsProductTemplate?.interestCalculationDaysInYearTypeOptions?.getOrNull( + state.daysInYearIndex, + )?.value.orEmpty() + ), + stringResource(Res.string.step_terms_apply_withdrawal_fee) + to ( + state.isCheckedApplyWithdrawalFee.let { + if (it) { + stringResource(Res.string.feature_savings_yes) + } else { + stringResource(Res.string.feature_savings_no) + } + } + ), + stringResource(Res.string.step_terms_is_allowed_overdraft) to + ( + state.isCheckedOverdraftAllowed.let { + if (it) { + stringResource(Res.string.feature_savings_yes) + } else { + stringResource(Res.string.feature_savings_no) + } + } + ), + stringResource(Res.string.step_terms_lock_in_period) to + if (state.freqTypeIndex == -1 || state.frequency.toIntOrNull() == null) { + "" + } else { + state.savingsProductTemplate?.lockinPeriodFrequencyTypeOptions + ?.getOrNull(state.freqTypeIndex)?.value + ?.let { "${state.frequency} $it" }.orEmpty() + }, + stringResource(Res.string.step_terms_minimum_balance) to state.monthlyMinimumBalance, + stringResource(Res.string.step_terms_min_opening_balance) to state.minimumOpeningBalance, + + ) + + Column(modifier = Modifier.fillMaxSize()) { + LazyColumn(modifier = modifier.weight(1f)) { + item { + Text( + stringResource(Res.string.step_details), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + + item { + MifosGeneralCard( + contentMap = previewDetailsMap, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + + item { + Text( + stringResource(Res.string.step_terms), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + + item { + MifosGeneralCard( + contentMap = termsDetailsMap, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + + item { + Text( + stringResource(Res.string.step_charges), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + + item { + MifosRowWithTextAndButton( + onBtnClick = { + onAction(SavingsAccountAction.ShowCharges) + }, + btnText = stringResource(Res.string.step_charges_view), + text = stringResource( + Res.string.feature_savings_charges_active_count, + state.addedCharges.size, + ), + btnEnabled = state.addedCharges.isNotEmpty(), + ) + } } + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.feature_savings_back), + secondBtnText = stringResource(Res.string.feature_savings_submit), + onFirstBtnClick = { + onAction(SavingsAccountAction.PreviousStep) + }, + onSecondBtnClick = { + onAction(SavingsAccountAction.SubmitSavingsApplication) + }, + isSecondButtonEnabled = state.isTermsNextEnabled, + modifier = Modifier.padding(top = DesignToken.padding.small), + ) } } diff --git a/version.txt b/version.txt index 0dc54a3fed..91fd393753 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2025.9.2-beta.0.5 \ No newline at end of file +2025.10.4-beta.0.6 \ No newline at end of file