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 4873dfa4a48..3c038e2f6e6 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 @@ -65,6 +65,7 @@ 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.GetSavingsProductTemplateUseCase import com.mifos.core.domain.useCases.GetStaffInOfficeUseCase import com.mifos.core.domain.useCases.GetUserPathTrackingUseCase import com.mifos.core.domain.useCases.GroupsListPagingDataSource @@ -105,6 +106,7 @@ val UseCaseModule = module { factoryOf(::CreateLoanAccountUseCase) factoryOf(::CreateLoanChargesUseCase) factoryOf(::CreateSavingsAccountUseCase) + factoryOf(::GetSavingsProductTemplateUseCase) factoryOf(::GetClientTemplateUseCase) factoryOf(::DeleteCheckerUseCase) factoryOf(::DeleteClientAddressPinpointUseCase) diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsProductTemplateUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsProductTemplateUseCase.kt new file mode 100644 index 00000000000..29a308fcd70 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetSavingsProductTemplateUseCase.kt @@ -0,0 +1,23 @@ +/* + * 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.SavingsAccountRepository +import com.mifos.room.entities.templates.savings.SavingProductsTemplate +import kotlinx.coroutines.flow.Flow + +class GetSavingsProductTemplateUseCase( + private val repository: SavingsAccountRepository, +) { + operator fun invoke(): Flow> { + return repository.getSavingsAccountTemplate() + } +} 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 2d26a865fac..326b19ca539 100644 --- a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml +++ b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml @@ -100,10 +100,37 @@ Savings Account Id View Receipt + Details Terms Charges Preview + + + Minimum balance must be a positive number + Minimum balance is required + Lock-in period type is required + Lock-in period frequency must be a positive number + Minimum opening balance must be a positive number + Days in year is required + Interest calculation method is required + Interest posting period is required + Interest compounding period is required + Decimal places must be between 0 and 6 + Currency is required + + Minimum Balance + Enforce Minimum Balance + Monthly Minimum Balance + is Overdraft Allowed + Overdraft + Decimal Places + Type + Frequency + Lock-in Period + Apply Withdrawal Fee for Transfers + Minimum Opening Balance + 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 c0a41951e32..2c495272fd3 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 @@ -85,9 +85,10 @@ private fun SavingsAccountScaffold( ) }, Step(stringResource(Res.string.step_terms)) { - TermsPage { - onAction(SavingsAccountAction.NextStep) - } + TermsPage( + state = state, + onAction = onAction, + ) }, Step(stringResource(Res.string.step_charges)) { ChargesPage { 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 79aede34b1e..b59e71327fb 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 @@ -16,11 +16,13 @@ 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.GetClientTemplateUseCase +import com.mifos.core.domain.useCases.GetSavingsProductTemplateUseCase import com.mifos.core.ui.util.BaseViewModel import com.mifos.core.ui.util.TextFieldsValidator import com.mifos.room.entities.templates.clients.ClientsTemplateEntity import com.mifos.room.entities.templates.clients.SavingProductOptionsEntity import com.mifos.room.entities.templates.clients.StaffOptionsEntity +import com.mifos.room.entities.templates.savings.SavingProductsTemplate import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -31,6 +33,7 @@ import kotlin.time.ExperimentalTime internal class SavingsAccountViewModel( private val networkMonitor: NetworkMonitor, private val getClientTemplateUseCase: GetClientTemplateUseCase, + private val getSavingsProductTemplateUseCase: GetSavingsProductTemplateUseCase, val savedStateHandle: SavedStateHandle, ) : BaseViewModel( @@ -41,12 +44,14 @@ internal class SavingsAccountViewModel( init { loadClientTemplate() + loadSavingsProductTemplate() } override fun handleAction(action: SavingsAccountAction) { when (action) { is SavingsAccountAction.NavigateBack -> sendEvent(SavingsAccountEvent.NavigateBack) is SavingsAccountAction.NextStep -> moveToNextStep() + is SavingsAccountAction.PreviousStep -> moveToPreviousStep() is SavingsAccountAction.Finish -> sendEvent(SavingsAccountEvent.Finish) is SavingsAccountAction.OnStepChange -> handleStepChange(action) is SavingsAccountAction.OnSubmissionDatePick -> handleSubmissionDatePick(action) @@ -57,9 +62,153 @@ internal class SavingsAccountViewModel( is SavingsAccountAction.OnProductNameChange -> handleOnProductNameChange(action) is SavingsAccountAction.Internal.OnReceivingClientTemplate -> handleClientTemplateResponse(action.clientTemplate) is SavingsAccountAction.OnFieldOfficerChange -> handleFieldOfficerChange(action) + is SavingsAccountAction.OnDecimalPlacesChange -> handleDecimalPlacesChange(action) + is SavingsAccountAction.OnMinimumOpeningBalanceChange -> handleMinimumOpeningBalanceChange(action) + is SavingsAccountAction.OnFrequencyChange -> handleFrequencyChange(action) + is SavingsAccountAction.OnMonthlyMinimumBalanceChange -> handleMonthlyMinimumBalanceChange(action) + is SavingsAccountAction.OnApplyWithdrawalFeeChange -> handleApplyWithdrawalChange(action) + is SavingsAccountAction.OnMinimumBalanceChange -> handleMinimumBalanceChange(action) + is SavingsAccountAction.OnOverDraftAllowedChange -> handleApplyOverdraftChange(action) + is SavingsAccountAction.Internal.OnReceivingSavingsProductTemplate -> handleSavingsProductTemplate(action.savingsProductTemplate) + is SavingsAccountAction.OnCurrencyChange -> handleCurrencyChange(action) + is SavingsAccountAction.OnDaysInYearChange -> handleDaysInYearChange(action) + is SavingsAccountAction.OnFreqTypeChange -> handleFreqTypeChange(action) + is SavingsAccountAction.OnInterestCalcChange -> handleInterestCalcChange(action) + is SavingsAccountAction.OnInterestCompPeriodChange -> handleInterestCompPeriodChange(action) + is SavingsAccountAction.OnInterestPostingPeriodChange -> handleInterestPostingPeriodChange(action) + is SavingsAccountAction.SetCurrencyError -> handleCurrencyError(action) + is SavingsAccountAction.SetDecimalPlacesError -> handleDecimalPlacesError(action) + is SavingsAccountAction.SetInterestCompPeriodError -> handleInterestCompPeriodError(action) + is SavingsAccountAction.SetInterestPostingPeriodError -> handleInterestPostingPeriodError(action) + is SavingsAccountAction.SetInterestCalcError -> handleInterestCalcError(action) + is SavingsAccountAction.SetDaysInYearError -> handleDaysInYearError(action) + is SavingsAccountAction.OnMinimumOpeningBalanceError -> handleMinimumOpeningBalanceError(action) + is SavingsAccountAction.SetFrequencyError -> handleFrequencyError(action) + is SavingsAccountAction.SetFreqTypeError -> handleFreqTypeError(action) + is SavingsAccountAction.OnMonthlyMinimumBalanceError -> handleMonthlyMinimumBalanceError(action) } } + private fun handleCurrencyError(action: SavingsAccountAction.SetCurrencyError) { + mutableStateFlow.update { it.copy(currencyError = action.message) } + } + + private fun handleDecimalPlacesError(action: SavingsAccountAction.SetDecimalPlacesError) { + mutableStateFlow.update { it.copy(decimalPlacesError = action.message) } + } + + private fun handleInterestCompPeriodError(action: SavingsAccountAction.SetInterestCompPeriodError) { + mutableStateFlow.update { it.copy(interestCompPeriodError = action.message) } + } + + private fun handleInterestPostingPeriodError(action: SavingsAccountAction.SetInterestPostingPeriodError) { + mutableStateFlow.update { it.copy(interestPostingPeriodError = action.message) } + } + + private fun handleInterestCalcError(action: SavingsAccountAction.SetInterestCalcError) { + mutableStateFlow.update { it.copy(interestCalcError = action.message) } + } + + private fun handleDaysInYearError(action: SavingsAccountAction.SetDaysInYearError) { + mutableStateFlow.update { it.copy(daysInYearError = action.message) } + } + + private fun handleMinimumOpeningBalanceError(action: SavingsAccountAction.OnMinimumOpeningBalanceError) { + mutableStateFlow.update { it.copy(minimumOpeningBalanceError = action.message) } + } + + private fun handleFrequencyError(action: SavingsAccountAction.SetFrequencyError) { + mutableStateFlow.update { it.copy(frequencyError = action.message) } + } + + private fun handleFreqTypeError(action: SavingsAccountAction.SetFreqTypeError) { + mutableStateFlow.update { it.copy(freqTypeError = action.message) } + } + + private fun handleMonthlyMinimumBalanceError(action: SavingsAccountAction.OnMonthlyMinimumBalanceError) { + mutableStateFlow.update { it.copy(monthlyMinimumBalanceError = action.message) } + } + + private fun handleInterestCalcChange(action: SavingsAccountAction.OnInterestCalcChange) { + mutableStateFlow.update { it.copy(interestCalcIndex = action.index) } + } + + private fun handleInterestCompPeriodChange(action: SavingsAccountAction.OnInterestCompPeriodChange) { + mutableStateFlow.update { it.copy(interestCompPeriodIndex = action.index) } + } + + private fun handleInterestPostingPeriodChange(action: SavingsAccountAction.OnInterestPostingPeriodChange) { + mutableStateFlow.update { it.copy(interestPostingPeriodIndex = action.index) } + } + + private fun handleFreqTypeChange(action: SavingsAccountAction.OnFreqTypeChange) { + mutableStateFlow.update { it.copy(freqTypeIndex = action.index) } + } + + private fun handleDaysInYearChange(action: SavingsAccountAction.OnDaysInYearChange) { + mutableStateFlow.update { it.copy(daysInYearIndex = action.index) } + } + + private fun handleCurrencyChange(action: SavingsAccountAction.OnCurrencyChange) { + mutableStateFlow.update { it.copy(currencyIndex = action.index) } + } + + private fun handleSavingsProductTemplate(result: DataState) { + when (result) { + is DataState.Loading -> mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.Loading, + ) + } + is DataState.Error -> mutableStateFlow.update { + it.copy( + dialogState = SavingsAccountState.DialogState.Error(result.message), + ) + } + is DataState.Success -> mutableStateFlow.update { + it.copy( + dialogState = null, + screenState = SavingsAccountState.ScreenState.Success, + savingsProductTemplate = result.data, + currencyIndex = result.data.currencyOptions?.indexOf(result.data.currency) ?: -1, + decimalPlaces = result.data.currency?.decimalPlaces?.toInt().toString(), + interestPostingPeriodIndex = result.data.interestPostingPeriodTypeOptions?.indexOf(result.data.interestPostingPeriodType) ?: -1, + interestCalcIndex = result.data.interestCalculationTypeOptions?.indexOf(result.data.interestCalculationType) ?: -1, + interestCompPeriodIndex = result.data.interestCompoundingPeriodTypeOptions?.indexOf(result.data.interestCompoundingPeriodType) ?: -1, + daysInYearIndex = result.data.interestCalculationDaysInYearTypeOptions?.indexOf(result.data.interestCalculationDaysInYearType) ?: -1, + ) + } + } + } + + private fun handleApplyWithdrawalChange(action: SavingsAccountAction.OnApplyWithdrawalFeeChange) { + mutableStateFlow.update { it.copy(isCheckedApplyWithdrawalFee = action.boolean) } + } + + private fun handleMinimumBalanceChange(action: SavingsAccountAction.OnMinimumBalanceChange) { + mutableStateFlow.update { it.copy(isCheckedMinimumBalance = action.boolean) } + } + + private fun handleApplyOverdraftChange(action: SavingsAccountAction.OnOverDraftAllowedChange) { + mutableStateFlow.update { it.copy(isCheckedOverdraftAllowed = action.boolean) } + } + + private fun handleFrequencyChange(action: SavingsAccountAction.OnFrequencyChange) { + mutableStateFlow.update { it.copy(frequency = action.value) } + } + + private fun handleMonthlyMinimumBalanceChange(action: SavingsAccountAction.OnMonthlyMinimumBalanceChange) { + mutableStateFlow.update { it.copy(monthlyMinimumBalance = action.value) } + } + + private fun handleMinimumOpeningBalanceChange(action: SavingsAccountAction.OnMinimumOpeningBalanceChange) { + mutableStateFlow.update { it.copy(minimumOpeningBalance = action.value) } + } + + private fun handleDecimalPlacesChange(action: SavingsAccountAction.OnDecimalPlacesChange) { + mutableStateFlow.update { it.copy(decimalPlaces = action.value) } + } + private fun handleFieldOfficerChange(action: SavingsAccountAction.OnFieldOfficerChange) { mutableStateFlow.update { it.copy(fieldOfficerIndex = action.index) } } @@ -139,6 +288,21 @@ internal class SavingsAccountViewModel( } } + private fun loadSavingsProductTemplate() = viewModelScope.launch { + val online = networkMonitor.isOnline.first() + if (online) { + getSavingsProductTemplateUseCase().collect { result -> + sendAction(SavingsAccountAction.Internal.OnReceivingSavingsProductTemplate(result)) + } + } else { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.NetworkError, + ) + } + } + } + private fun handleRetry() { mutableStateFlow.update { it.copy( @@ -148,6 +312,15 @@ internal class SavingsAccountViewModel( loadClientTemplate() } + private fun moveToPreviousStep() { + val current = state.currentStep + mutableStateFlow.update { + it.copy( + currentStep = current - 1, + ) + } + } + private fun moveToNextStep() { val current = state.currentStep if (current < state.totalSteps) { @@ -179,6 +352,30 @@ constructor( val screenState: ScreenState = ScreenState.Loading, val submissionDate: String = DateHelper.getDateAsStringFromLong(Clock.System.now().toEpochMilliseconds()), val showSubmissionDatePick: Boolean = false, + val decimalPlaces: String = "", + val decimalPlacesError: String? = null, + val minimumOpeningBalance: String = "", + val minimumOpeningBalanceError: String? = null, + val frequency: String = "", + val frequencyError: String? = null, + val monthlyMinimumBalance: String = "", + val monthlyMinimumBalanceError: String? = null, + val isCheckedApplyWithdrawalFee: Boolean = false, + val isCheckedOverdraftAllowed: Boolean = false, + val isCheckedMinimumBalance: Boolean = false, + val savingsProductTemplate: SavingProductsTemplate? = null, + val currencyIndex: Int = -1, + val currencyError: String? = null, + val interestCompPeriodIndex: Int = -1, + val interestCompPeriodError: String? = null, + val interestPostingPeriodIndex: Int = -1, + val interestPostingPeriodError: String? = null, + val interestCalcIndex: Int = -1, + val interestCalcError: String? = null, + val daysInYearIndex: Int = -1, + val daysInYearError: String? = null, + val freqTypeIndex: Int = -1, + val freqTypeError: String? = null, ) { sealed interface DialogState { data class Error(val message: String) : DialogState @@ -193,6 +390,12 @@ constructor( val isDetailsNextEnabled = submissionDate.isNotEmpty() && savingsProductSelected != -1 && fieldOfficerIndex != -1 + + val isTermsNextEnabled = isDetailsNextEnabled && + currencyIndex != -1 && + interestCalcIndex != -1 && + interestPostingPeriodIndex != -1 && + interestCompPeriodIndex != -1 } sealed interface SavingsAccountEvent { @@ -203,6 +406,7 @@ sealed interface SavingsAccountEvent { sealed interface SavingsAccountAction { data object NavigateBack : SavingsAccountAction data object NextStep : SavingsAccountAction + data object PreviousStep : SavingsAccountAction data object Finish : SavingsAccountAction data class OnStepChange(val newIndex: Int) : SavingsAccountAction data class OnSubmissionDateChange(val date: String) : SavingsAccountAction @@ -211,9 +415,33 @@ sealed interface SavingsAccountAction { data class OnProductNameChange(val index: Int) : SavingsAccountAction data class OnFieldOfficerChange(val index: Int) : SavingsAccountAction data class OnExternalIdChange(val value: String) : SavingsAccountAction + data class OnDecimalPlacesChange(val value: String) : SavingsAccountAction + data class OnMinimumOpeningBalanceChange(val value: String) : SavingsAccountAction + data class OnFrequencyChange(val value: String) : SavingsAccountAction + data class OnMonthlyMinimumBalanceChange(val value: String) : SavingsAccountAction + data class OnApplyWithdrawalFeeChange(val boolean: Boolean) : SavingsAccountAction + data class OnOverDraftAllowedChange(val boolean: Boolean) : SavingsAccountAction + data class OnMinimumBalanceChange(val boolean: Boolean) : SavingsAccountAction + data class OnCurrencyChange(val index: Int) : SavingsAccountAction + data class OnInterestCompPeriodChange(val index: Int) : SavingsAccountAction + data class OnInterestPostingPeriodChange(val index: Int) : SavingsAccountAction + data class OnInterestCalcChange(val index: Int) : SavingsAccountAction + data class OnDaysInYearChange(val index: Int) : SavingsAccountAction + data class OnFreqTypeChange(val index: Int) : SavingsAccountAction data object Retry : SavingsAccountAction + data class SetCurrencyError(val message: String?) : SavingsAccountAction + data class SetDecimalPlacesError(val message: String?) : SavingsAccountAction + data class SetInterestCompPeriodError(val message: String?) : SavingsAccountAction + data class SetInterestPostingPeriodError(val message: String?) : SavingsAccountAction + data class SetInterestCalcError(val message: String?) : SavingsAccountAction + data class SetDaysInYearError(val message: String?) : SavingsAccountAction + data class OnMinimumOpeningBalanceError(val message: String?) : SavingsAccountAction + data class SetFrequencyError(val message: String?) : SavingsAccountAction + data class SetFreqTypeError(val message: String?) : SavingsAccountAction + data class OnMonthlyMinimumBalanceError(val message: String?) : SavingsAccountAction sealed interface Internal : SavingsAccountAction { data class OnReceivingClientTemplate(val clientTemplate: DataState) : Internal + data class OnReceivingSavingsProductTemplate(val savingsProductTemplate: DataState) : Internal } } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt index e335bee6bef..eb6f4e682f4 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/TermsPage.kt @@ -10,26 +10,463 @@ package com.mifos.feature.savings.savingsAccountv2.pages import androidclient.feature.savings.generated.resources.Res -import androidclient.feature.savings.generated.resources.feature_savings_submit +import androidclient.feature.savings.generated.resources.feature_savings_back +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_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_next 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_currency_required +import androidclient.feature.savings.generated.resources.step_terms_days_in_year_required +import androidclient.feature.savings.generated.resources.step_terms_decimal_places +import androidclient.feature.savings.generated.resources.step_terms_decimal_places_error +import androidclient.feature.savings.generated.resources.step_terms_enforce_min_balance +import androidclient.feature.savings.generated.resources.step_terms_frequency +import androidclient.feature.savings.generated.resources.step_terms_interest_calc_required +import androidclient.feature.savings.generated.resources.step_terms_interest_comp_period_required +import androidclient.feature.savings.generated.resources.step_terms_interest_posting_period_required +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_lock_in_period_freq_error +import androidclient.feature.savings.generated.resources.step_terms_lock_in_type_required +import androidclient.feature.savings.generated.resources.step_terms_min_balance_required +import androidclient.feature.savings.generated.resources.step_terms_min_monthly_balance_error +import androidclient.feature.savings.generated.resources.step_terms_min_opening_balance +import androidclient.feature.savings.generated.resources.step_terms_min_opening_balance_error +import androidclient.feature.savings.generated.resources.step_terms_minimum_balance +import androidclient.feature.savings.generated.resources.step_terms_monthly_min_balance +import androidclient.feature.savings.generated.resources.step_terms_overdraft +import androidclient.feature.savings.generated.resources.step_terms_type +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.material3.Button +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Checkbox import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosTextFieldConfig +import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTypography +import com.mifos.core.ui.components.MifosTwoButtonRow +import com.mifos.feature.savings.savingsAccountv2.SavingsAccountAction +import com.mifos.feature.savings.savingsAccountv2.SavingsAccountState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource @Composable -fun TermsPage(onNext: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(stringResource(Res.string.step_terms)) - Spacer(Modifier.height(8.dp)) - Button(onClick = onNext) { - Text(stringResource(Res.string.feature_savings_submit)) +fun TermsPage( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + + LaunchedEffect( + state.currencyIndex, + state.frequency, + state.monthlyMinimumBalance, + state.minimumOpeningBalance, + state.decimalPlaces, + state.interestCalcIndex, + state.interestCompPeriodIndex, + state.interestPostingPeriodIndex, + state.daysInYearIndex, + state.freqTypeIndex, + state.isCheckedMinimumBalance, + ) { + validateAllFields(state, onAction) + } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = modifier.weight(1f).verticalScroll(rememberScrollState()), + ) { + Text( + stringResource(Res.string.step_terms), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + MifosTextFieldDropdown( + value = if (state.currencyIndex == -1) { + "" + } else { + state.savingsProductTemplate?.currencyOptions?.get(state.currencyIndex)?.name ?: "" + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnCurrencyChange(index)) + }, + options = state.savingsProductTemplate?.currencyOptions?.map { currency -> + currency.name ?: "" + } ?: emptyList(), + label = stringResource(Res.string.feature_savings_currency), + errorMessage = state.currencyError, + ) + MifosOutlinedTextField( + value = state.decimalPlaces, + onValueChange = { + onAction(SavingsAccountAction.OnDecimalPlacesChange(it)) + }, + label = stringResource(Res.string.step_terms_decimal_places), + config = MifosTextFieldConfig( + isError = state.decimalPlacesError != null, + errorText = state.decimalPlacesError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + ), + ) + Spacer(Modifier.height(DesignToken.padding.large)) + MifosTextFieldDropdown( + value = if (state.interestCompPeriodIndex == -1) { + "" + } else { + state.savingsProductTemplate?.interestCompoundingPeriodTypeOptions?.get(state.interestCompPeriodIndex)?.value + ?: "" + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnInterestCompPeriodChange(index)) + }, + options = state.savingsProductTemplate?.interestCompoundingPeriodTypeOptions?.map { interestType -> + interestType.value ?: "" + } ?: emptyList(), + label = stringResource(Res.string.feature_savings_interest_comp), + errorMessage = state.interestCompPeriodError, + ) + MifosTextFieldDropdown( + value = if (state.interestPostingPeriodIndex == -1) { + "" + } else { + state.savingsProductTemplate?.interestPostingPeriodTypeOptions?.get(state.interestPostingPeriodIndex)?.value + ?: "" + }, + onValueChanged = { }, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnInterestPostingPeriodChange(index)) + }, + options = state.savingsProductTemplate?.interestPostingPeriodTypeOptions?.map { postingPeriod -> + postingPeriod.value ?: "" + } ?: emptyList(), + label = stringResource(Res.string.feature_savings_interest_p_period), + errorMessage = state.interestPostingPeriodError, + ) + MifosTextFieldDropdown( + value = if (state.interestCalcIndex == -1) { + "" + } else { + state.savingsProductTemplate?.interestCalculationTypeOptions?.get(state.interestCalcIndex)?.value + ?: "" + }, + onValueChanged = { }, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnInterestCalcChange(index)) + }, + options = state.savingsProductTemplate?.interestCalculationTypeOptions?.map { calcType -> + calcType.value ?: "" + } ?: emptyList(), + label = stringResource(Res.string.feature_savings_interest_calc), + errorMessage = state.interestCalcError, + ) + MifosTextFieldDropdown( + value = if (state.daysInYearIndex == -1) { + "" + } else { + state.savingsProductTemplate?.interestCalculationDaysInYearTypeOptions?.get(state.daysInYearIndex)?.value + ?: "" + }, + onValueChanged = { }, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnDaysInYearChange(index)) + }, + options = state.savingsProductTemplate?.interestCalculationDaysInYearTypeOptions?.map { daysInYearType -> + daysInYearType.value ?: "" + } ?: emptyList(), + label = stringResource(Res.string.feature_savings_days_in_year), + errorMessage = state.daysInYearError, + ) + MifosOutlinedTextField( + value = state.minimumOpeningBalance, + onValueChange = { onAction(SavingsAccountAction.OnMinimumOpeningBalanceChange(it)) }, + label = stringResource(Res.string.step_terms_min_opening_balance), + config = MifosTextFieldConfig( + isError = state.minimumOpeningBalanceError != null, + errorText = state.minimumOpeningBalanceError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + ), + ) + Spacer(Modifier.height(DesignToken.padding.large)) + Row( + Modifier.fillMaxWidth() + .clickable { + onAction(SavingsAccountAction.OnApplyWithdrawalFeeChange(!state.isCheckedApplyWithdrawalFee)) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = state.isCheckedApplyWithdrawalFee, + onCheckedChange = { + onAction(SavingsAccountAction.OnApplyWithdrawalFeeChange(it)) + }, + ) + Text( + text = stringResource(Res.string.step_terms_apply_withdrawal_fee), + style = MifosTypography.labelLarge, + ) + } + Spacer(Modifier.height(DesignToken.padding.large)) + Text( + stringResource(Res.string.step_terms_lock_in_period), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + MifosOutlinedTextField( + value = state.frequency, + onValueChange = { onAction(SavingsAccountAction.OnFrequencyChange(it)) }, + label = stringResource(Res.string.step_terms_frequency), + config = MifosTextFieldConfig( + isError = state.frequencyError != null, + errorText = state.frequencyError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + ), + ) + Spacer(Modifier.height(DesignToken.padding.large)) + MifosTextFieldDropdown( + value = if (state.freqTypeIndex == -1) { + "" + } else { + state.savingsProductTemplate?.lockinPeriodFrequencyTypeOptions?.get(state.freqTypeIndex)?.value + ?: "" + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnFreqTypeChange(index)) + }, + options = state.savingsProductTemplate?.lockinPeriodFrequencyTypeOptions?.map { freqType -> + freqType.value ?: "" + } ?: emptyList(), + label = stringResource(Res.string.step_terms_type), + enabled = state.frequency.isNotEmpty(), + errorMessage = state.freqTypeError, + ) + Text( + stringResource(Res.string.step_terms_overdraft), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + Row( + Modifier.fillMaxWidth() + .clickable { + onAction(SavingsAccountAction.OnOverDraftAllowedChange(!state.isCheckedOverdraftAllowed)) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = state.isCheckedOverdraftAllowed, + onCheckedChange = { + onAction(SavingsAccountAction.OnOverDraftAllowedChange(it)) + }, + ) + Text( + text = stringResource(Res.string.step_terms_is_allowed_overdraft), + style = MifosTypography.labelLarge, + ) + } + Spacer(Modifier.height(DesignToken.padding.large)) + Text( + stringResource(Res.string.step_terms_monthly_min_balance), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + Row( + Modifier.fillMaxWidth() + .clickable { + onAction(SavingsAccountAction.OnMinimumBalanceChange(!state.isCheckedMinimumBalance)) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = state.isCheckedMinimumBalance, + onCheckedChange = { + onAction(SavingsAccountAction.OnMinimumBalanceChange(it)) + }, + ) + Text( + text = stringResource(Res.string.step_terms_enforce_min_balance), + style = MifosTypography.labelLarge, + ) + } + Spacer(Modifier.height(DesignToken.padding.large)) + MifosOutlinedTextField( + value = state.monthlyMinimumBalance, + onValueChange = { onAction(SavingsAccountAction.OnMonthlyMinimumBalanceChange(it)) }, + label = stringResource(Res.string.step_terms_minimum_balance), + config = MifosTextFieldConfig( + enabled = state.isCheckedMinimumBalance, + isError = state.monthlyMinimumBalanceError != null, + errorText = state.monthlyMinimumBalanceError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + ), + ) + Spacer(Modifier.height(DesignToken.padding.large)) + } + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.feature_savings_back), + secondBtnText = stringResource(Res.string.feature_savings_next), + onFirstBtnClick = { + onAction(SavingsAccountAction.PreviousStep) + }, + onSecondBtnClick = { + handleNext( + state, + onAction, + scope, + ) + }, + isSecondButtonEnabled = state.isTermsNextEnabled, + modifier = Modifier.padding(top = DesignToken.padding.small), + ) + } +} + +private fun handleNext( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, + scope: CoroutineScope, +) { + scope.launch { + val isValid = validateAllFields(state, onAction) + if (isValid) { + onAction(SavingsAccountAction.NextStep) + } + } +} + +private suspend fun validateAllFields( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, +): Boolean { + var isValid = true + if (state.currencyIndex == -1) { + onAction(SavingsAccountAction.SetCurrencyError(getString(Res.string.step_terms_currency_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetCurrencyError(null)) + } + + val decimalPlaces = state.decimalPlaces.toIntOrNull() + if (decimalPlaces == null || decimalPlaces < 0 || decimalPlaces > 6 || state.decimalPlaces.length != 1) { + onAction(SavingsAccountAction.SetDecimalPlacesError(getString(Res.string.step_terms_decimal_places_error))) + isValid = false + } else { + onAction(SavingsAccountAction.SetDecimalPlacesError(null)) + } + + if (state.interestCompPeriodIndex == -1) { + onAction(SavingsAccountAction.SetInterestCompPeriodError(getString(Res.string.step_terms_interest_comp_period_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetInterestCompPeriodError(null)) + } + + if (state.interestPostingPeriodIndex == -1) { + onAction(SavingsAccountAction.SetInterestPostingPeriodError(getString(Res.string.step_terms_interest_posting_period_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetInterestPostingPeriodError(null)) + } + + if (state.interestCalcIndex == -1) { + onAction(SavingsAccountAction.SetInterestCalcError(getString(Res.string.step_terms_interest_calc_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetInterestCalcError(null)) + } + + if (state.daysInYearIndex == -1) { + onAction(SavingsAccountAction.SetDaysInYearError(getString(Res.string.step_terms_days_in_year_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetDaysInYearError(null)) + } + + if (state.minimumOpeningBalance.isNotEmpty()) { + val minimumOpeningBalance = state.minimumOpeningBalance.toDoubleOrNull() + if (minimumOpeningBalance == null || minimumOpeningBalance < 0) { + onAction(SavingsAccountAction.OnMinimumOpeningBalanceError(getString(Res.string.step_terms_min_opening_balance_error))) + isValid = false + } else { + onAction(SavingsAccountAction.OnMinimumOpeningBalanceError(null)) + } + } else { + onAction(SavingsAccountAction.OnMinimumOpeningBalanceError(null)) + } + + if (state.frequency.isNotEmpty()) { + val frequency = state.frequency.toIntOrNull() + if (frequency == null || frequency < 0) { + onAction(SavingsAccountAction.SetFrequencyError(getString(Res.string.step_terms_lock_in_period_freq_error))) + isValid = false + } else { + onAction(SavingsAccountAction.SetFrequencyError(null)) + } + + if (state.freqTypeIndex == -1) { + onAction(SavingsAccountAction.SetFreqTypeError(getString(Res.string.step_terms_lock_in_type_required))) + isValid = false + } else { + onAction(SavingsAccountAction.SetFreqTypeError(null)) + } + } else { + onAction(SavingsAccountAction.SetFrequencyError(null)) + onAction(SavingsAccountAction.SetFreqTypeError(null)) + } + + if (state.isCheckedMinimumBalance) { + if (state.monthlyMinimumBalance.isEmpty()) { + onAction(SavingsAccountAction.OnMonthlyMinimumBalanceError(getString(Res.string.step_terms_min_balance_required))) + isValid = false + } else { + val minimumBalance = state.monthlyMinimumBalance.toDoubleOrNull() + if (minimumBalance == null || minimumBalance < 0) { + onAction(SavingsAccountAction.OnMonthlyMinimumBalanceError(getString(Res.string.step_terms_min_monthly_balance_error))) + isValid = false + } else { + onAction(SavingsAccountAction.OnMonthlyMinimumBalanceError(null)) + } } + } else { + onAction(SavingsAccountAction.OnMonthlyMinimumBalanceError(null)) } + return isValid }