diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt index bada715a448..78fceaedb2b 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt @@ -118,6 +118,7 @@ internal fun AuthenticatedNavbarNavigationScreen( name = "AuthenticatedNavbarScreen", ), navigateToNewLoanAccountScreen: (Int) -> Unit, + navigateToNewSavingsAccountScreen: (Int) -> Unit, viewModel: AuthenticatedNavbarNavigationViewModel = koinViewModel(), ) { val scope = rememberCoroutineScope() @@ -179,6 +180,7 @@ internal fun AuthenticatedNavbarNavigationScreen( navigateToDocumentScreen = navigateToDocumentScreen, navigateToNoteScreen = navigateToNoteScreen, navigateToNewLoanAccountScreen = navigateToNewLoanAccountScreen, + navigateToNewSavingsAccountScreen = navigateToNewSavingsAccountScreen, ) } @@ -190,6 +192,7 @@ internal fun AuthenticatedNavbarNavigationScreenContent( navigateToDocumentScreen: (Int, String) -> Unit, navigateToNoteScreen: (Int, String) -> Unit, navigateToNewLoanAccountScreen: (Int) -> Unit, + navigateToNewSavingsAccountScreen: (Int) -> Unit, modifier: Modifier = Modifier, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, onAction: (AuthenticatedNavBarAction) -> Unit, @@ -433,6 +436,7 @@ internal fun AuthenticatedNavbarNavigationScreenContent( hasDatatables = navController::navigateDataTableList, onDocumentClicked = navigateToDocumentScreen, navigateToNewLoanAccount = navigateToNewLoanAccountScreen, + navigateToNewSavingsAccount = navigateToNewSavingsAccountScreen, ) } } diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt index 5b77211c84d..5aa5bd7f3f3 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt @@ -27,6 +27,7 @@ internal fun NavGraphBuilder.authenticatedNavbarGraph( navigateToDocumentScreen: (Int, String) -> Unit, navigateToNoteScreen: (Int, String) -> Unit, navigateToNewLoanAccountScreen: (Int) -> Unit, + navigateToNewSavingsAccountScreen: (Int) -> Unit, ) { composable { AuthenticatedNavbarNavigationScreen( @@ -34,6 +35,7 @@ internal fun NavGraphBuilder.authenticatedNavbarGraph( navigateToDocumentScreen = navigateToDocumentScreen, navigateToNoteScreen = navigateToNoteScreen, navigateToNewLoanAccountScreen = navigateToNewLoanAccountScreen, + navigateToNewSavingsAccountScreen = navigateToNewSavingsAccountScreen, ) } } diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt index f5ebb9df2d8..928abab4538 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt @@ -33,6 +33,7 @@ import com.mifos.feature.offline.navigation.offlineNavGraph import com.mifos.feature.path.tracking.navigation.pathTrackingRoute import com.mifos.feature.report.navigation.reportNavGraph import com.mifos.feature.savings.navigation.savingsNavGraph +import com.mifos.feature.savings.savingsAccountv2.navigateToSavingsAccountRoute import com.mifos.feature.settings.navigation.settingsScreen import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi @@ -61,6 +62,7 @@ internal fun NavGraphBuilder.authenticatedGraph( navigateToDocumentScreen = navController::navigateToDocumentListScreen, navigateToNoteScreen = navController::navigateToNoteScreen, navigateToNewLoanAccountScreen = navController::navigateToNewLoanAccountRoute, + navigateToNewSavingsAccountScreen = navController::navigateToSavingsAccountRoute, ) checkerInboxTaskNavGraph(navController) 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 2e4ec16198d..4c82495682c 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt @@ -46,6 +46,7 @@ import com.mifos.core.domain.useCases.GetClientDetailsUseCase import com.mifos.core.domain.useCases.GetClientIdentifierTemplateUseCase import com.mifos.core.domain.useCases.GetClientPinpointLocationsUseCase import com.mifos.core.domain.useCases.GetClientSavingsAccountTemplateByProductUseCase +import com.mifos.core.domain.useCases.GetClientTemplateUseCase import com.mifos.core.domain.useCases.GetDataTableInfoUseCase import com.mifos.core.domain.useCases.GetDocumentsListUseCase import com.mifos.core.domain.useCases.GetGroupDetailsUseCase @@ -104,6 +105,7 @@ val UseCaseModule = module { factoryOf(::CreateLoanAccountUseCase) factoryOf(::CreateLoanChargesUseCase) factoryOf(::CreateSavingsAccountUseCase) + factoryOf(::GetClientTemplateUseCase) factoryOf(::DeleteCheckerUseCase) factoryOf(::DeleteClientAddressPinpointUseCase) factoryOf(::DeleteDataTableEntryUseCase) diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientTemplateUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientTemplateUseCase.kt new file mode 100644 index 00000000000..5377b343ea2 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientTemplateUseCase.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.CreateNewClientRepository +import com.mifos.room.entities.templates.clients.ClientsTemplateEntity +import kotlinx.coroutines.flow.Flow + +class GetClientTemplateUseCase( + private val newClientRepository: CreateNewClientRepository, +) { + operator fun invoke(): Flow> { + return newClientRepository.clientTemplate() + } +} diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/TextFieldsValidator.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/TextFieldsValidator.kt index 89559168bb0..6409808a905 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/TextFieldsValidator.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/TextFieldsValidator.kt @@ -27,6 +27,13 @@ object TextFieldsValidator { } } + fun optionalStringValidator(input: String): StringResource? { + return when { + input.any { !it.isLetterOrDigit() && !it.isWhitespace() } -> Res.string.error_invalid_characters + else -> null // valid + } + } + fun numberValidator(input: String): StringResource? { return when { input.isBlank() -> Res.string.error_field_empty diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt index 4ddc269d0d7..38d69d23b80 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt @@ -22,7 +22,7 @@ data class ClientApplyNewApplicationRoute( fun NavGraphBuilder.clientApplyNewApplicationRoute( onNavigateBack: () -> Unit, onNavigateApplyLoanAccount: (Int) -> Unit, - onNavigateApplySavingsAccount: () -> Unit, + onNavigateApplySavingsAccount: (Int) -> Unit, onNavigateApplyShareAccount: () -> Unit, onNavigateApplyRecurringAccount: () -> Unit, onNavigateApplyFixedAccount: () -> Unit, diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationsScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationsScreen.kt index d2bbf3b9ae6..1af152e0c0d 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationsScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationsScreen.kt @@ -57,7 +57,7 @@ import org.koin.compose.viewmodel.koinViewModel internal fun ClientApplyNewApplicationsScreen( onNavigateBack: () -> Unit, onNavigateApplyLoanAccount: (Int) -> Unit, - onNavigateApplySavingsAccount: () -> Unit, + onNavigateApplySavingsAccount: (Int) -> Unit, onNavigateApplyShareAccount: () -> Unit, onNavigateApplyRecurringAccount: () -> Unit, onNavigateApplyFixedAccount: () -> Unit, @@ -74,7 +74,7 @@ internal fun ClientApplyNewApplicationsScreen( ClientApplyNewApplicationsItem.NewFixedAccount -> onNavigateApplyFixedAccount() ClientApplyNewApplicationsItem.NewLoanAccount -> onNavigateApplyLoanAccount(state.clientId) ClientApplyNewApplicationsItem.NewRecurringAccount -> onNavigateApplyRecurringAccount() - ClientApplyNewApplicationsItem.NewSavingsAccount -> onNavigateApplySavingsAccount() + ClientApplyNewApplicationsItem.NewSavingsAccount -> onNavigateApplySavingsAccount(state.clientId) ClientApplyNewApplicationsItem.NewShareAccount -> onNavigateApplyShareAccount() } } 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 ad07b2caa57..b07af642159 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 @@ -91,6 +91,7 @@ fun NavGraphBuilder.clientNavGraph( hasDatatables: KFunction4, Any?, Int, MutableList>, Unit>, onDocumentClicked: (Int, String) -> Unit, navigateToNewLoanAccount: (Int) -> Unit, + navigateToNewSavingsAccount: (Int) -> Unit, ) { navigation( startDestination = ClientListScreenRoute, @@ -252,7 +253,7 @@ fun NavGraphBuilder.clientNavGraph( clientApplyNewApplicationRoute( onNavigateBack = navController::popBackStack, onNavigateApplyLoanAccount = navigateToNewLoanAccount, - onNavigateApplySavingsAccount = { }, + onNavigateApplySavingsAccount = navigateToNewSavingsAccount, onNavigateApplyShareAccount = { }, onNavigateApplyRecurringAccount = { }, onNavigateApplyFixedAccount = { }, 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 aae0d047649..2d26a865fac 100644 --- a/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml +++ b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml @@ -60,6 +60,11 @@ Go Back Savings Products Submit + Select + ExternalId must be unique + Submission Date + Next + Back Add Savings Account The Savings Account has been submitted forApproval Field Officer diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/di/SavingsModule.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/di/SavingsModule.kt index e135038fb05..086e47ba296 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/di/SavingsModule.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/di/SavingsModule.kt @@ -15,11 +15,13 @@ import com.mifos.feature.savings.savingsAccountApproval.SavingsAccountApprovalVi import com.mifos.feature.savings.savingsAccountSummary.SavingsAccountSummaryViewModel import com.mifos.feature.savings.savingsAccountTransaction.SavingsAccountTransactionViewModel import com.mifos.feature.savings.savingsAccountTransactionReceipt.SavingsAccountTransactionReceiptViewModel +import com.mifos.feature.savings.savingsAccountv2.SavingsAccountViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val SavingsModule = module { viewModelOf(::SavingAccountViewModel) + viewModelOf(::SavingsAccountViewModel) viewModelOf(::SavingsAccountActivateViewModel) viewModelOf(::SavingsAccountApprovalViewModel) viewModelOf(::SavingsAccountSummaryViewModel) 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 fccb4655c16..27050dcf0e8 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 @@ -85,7 +85,11 @@ fun NavGraphBuilder.savingsNavGraph( onBackPressed() } - savingsAccountDestination() + savingsAccountDestination( + navController = navController, + onNavigateBack = onBackPressed, + onFinish = onBackPressed, + ) } } 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 453e0d2282e..e91ecd128cd 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 @@ -15,19 +15,26 @@ import androidx.navigation.compose.composable import kotlinx.serialization.Serializable @Serializable -data object SavingsAccountRoute +data class SavingsAccountRoute( + val clientId: Int = -1, +) -fun NavGraphBuilder.savingsAccountDestination() { +fun NavGraphBuilder.savingsAccountDestination( + navController: NavController, + onNavigateBack: () -> Unit, + onFinish: () -> Unit, +) { composable { SavingsAccountScreen( - onNavigateBack = {}, - onFinish = {}, + onNavigateBack = onNavigateBack, + onFinish = onFinish, + navController = navController, ) } } -fun NavController.navigateToSavingsAccountRoute() { +fun NavController.navigateToSavingsAccountRoute(clientId: Int) { this.navigate( - SavingsAccountRoute, + SavingsAccountRoute(clientId = clientId), ) } 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 d321d74c123..c0a41951e32 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 @@ -11,17 +11,25 @@ package com.mifos.feature.savings.savingsAccountv2 import androidclient.feature.savings.generated.resources.Res 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.step_charges import androidclient.feature.savings.generated.resources.step_details import androidclient.feature.savings.generated.resources.step_preview import androidclient.feature.savings.generated.resources.step_terms +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +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.MifosStepper import com.mifos.core.ui.components.Step import com.mifos.core.ui.util.EventsEffect @@ -30,13 +38,15 @@ import com.mifos.feature.savings.savingsAccountv2.pages.DetailsPage import com.mifos.feature.savings.savingsAccountv2.pages.PreviewPage import com.mifos.feature.savings.savingsAccountv2.pages.TermsPage import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel @Composable internal fun SavingsAccountScreen( + navController: NavController, onNavigateBack: () -> Unit, onFinish: () -> Unit, modifier: Modifier = Modifier, - viewModel: SavingsAccountViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), + viewModel: SavingsAccountViewModel = koinViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -47,24 +57,32 @@ internal fun SavingsAccountScreen( } } + NewSavingsAccountDialog( + state = state, + onAction = { viewModel.trySendAction(it) }, + ) + SavingsAccountScaffold( modifier = modifier, state = state, onAction = { viewModel.trySendAction(it) }, + navController = navController, ) } @Composable private fun SavingsAccountScaffold( + navController: NavController, state: SavingsAccountState, modifier: Modifier = Modifier, onAction: (SavingsAccountAction) -> Unit, ) { val steps = listOf( Step(stringResource(Res.string.step_details)) { - DetailsPage { - onAction(SavingsAccountAction.NextStep) - } + DetailsPage( + state = state, + onAction = onAction, + ) }, Step(stringResource(Res.string.step_terms)) { TermsPage { @@ -88,17 +106,51 @@ private fun SavingsAccountScaffold( onBackPressed = { onAction(SavingsAccountAction.NavigateBack) }, modifier = modifier, ) { paddingValues -> - if (state.dialogState == null) { - MifosStepper( - steps = steps, - currentIndex = state.currentStep, - onStepChange = { newIndex -> - onAction(SavingsAccountAction.OnStepChange(newIndex)) - }, - modifier = Modifier - .fillMaxWidth() - .padding(paddingValues), + when (state.screenState) { + is SavingsAccountState.ScreenState.Loading -> MifosProgressIndicator() + is SavingsAccountState.ScreenState.Success -> { + Column( + Modifier.fillMaxSize().padding(paddingValues), + ) { + MifosBreadcrumbNavBar( + navController, + ) + MifosStepper( + steps = steps, + currentIndex = state.currentStep, + onStepChange = { newIndex -> + onAction(SavingsAccountAction.OnStepChange(newIndex)) + }, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + is SavingsAccountState.ScreenState.NetworkError -> { + MifosSweetError( + message = stringResource(Res.string.feature_savings_error_not_connected_internet), + onclick = { onAction(SavingsAccountAction.Retry) }, + ) + } + } + if (state.isOverLayLoadingActive) { + MifosProgressIndicatorOverlay() + } + } +} + +@Composable +private fun NewSavingsAccountDialog( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, +) { + when (state.dialogState) { + is SavingsAccountState.DialogState.Error -> { + MifosSweetError( + message = state.dialogState.message, + onclick = { onAction(SavingsAccountAction.Retry) }, ) } + null -> Unit } } 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 2c36356e21c..498f72d4024 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,25 +9,144 @@ */ package com.mifos.feature.savings.savingsAccountv2 +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.GetClientTemplateUseCase 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 kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import org.jetbrains.compose.resources.StringResource -internal class SavingsAccountViewModel : +internal class SavingsAccountViewModel( + private val networkMonitor: NetworkMonitor, + private val getClientTemplateUseCase: GetClientTemplateUseCase, + val savedStateHandle: SavedStateHandle, +) : BaseViewModel( - initialState = SavingsAccountState(), + initialState = run { + SavingsAccountState(clientId = savedStateHandle.toRoute().clientId) + }, ) { + init { + loadClientTemplate() + } + override fun handleAction(action: SavingsAccountAction) { when (action) { - SavingsAccountAction.NavigateBack -> sendEvent(SavingsAccountEvent.NavigateBack) - SavingsAccountAction.NextStep -> moveToNextStep() - SavingsAccountAction.Finish -> sendEvent(SavingsAccountEvent.Finish) - is SavingsAccountAction.OnStepChange -> - mutableStateFlow.value = - mutableStateFlow.value.copy(currentStep = action.newIndex) + is SavingsAccountAction.NavigateBack -> sendEvent(SavingsAccountEvent.NavigateBack) + is SavingsAccountAction.NextStep -> moveToNextStep() + is SavingsAccountAction.Finish -> sendEvent(SavingsAccountEvent.Finish) + is SavingsAccountAction.OnStepChange -> handleStepChange(action) + is SavingsAccountAction.OnSubmissionDatePick -> handleSubmissionDatePick(action) + is SavingsAccountAction.Retry -> handleRetry() + is SavingsAccountAction.OnSubmissionDateChange -> handleSubmissionDateChange(action) + is SavingsAccountAction.OnDetailsSubmit -> handleOnDetailsSubmit() + is SavingsAccountAction.OnExternalIdChange -> handleExternalIdChange(action) + is SavingsAccountAction.OnProductNameChange -> handleOnProductNameChange(action) + is SavingsAccountAction.Internal.OnReceivingClientTemplate -> handleClientTemplateResponse(action.clientTemplate) + is SavingsAccountAction.OnFieldOfficerChange -> handleFieldOfficerChange(action) + } + } + + private fun handleFieldOfficerChange(action: SavingsAccountAction.OnFieldOfficerChange) { + mutableStateFlow.update { it.copy(fieldOfficerIndex = action.index) } + } + + private fun handleClientTemplateResponse(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, + savingProductOptions = result.data.savingProductOptions ?: emptyList(), + fieldOfficerOptions = result.data.staffOptions ?: emptyList(), + ) + } + } + } + + private fun handleOnProductNameChange(action: SavingsAccountAction.OnProductNameChange) { + mutableStateFlow.update { it.copy(savingsProductSelected = action.index) } + } + + private fun handleExternalIdChange(action: SavingsAccountAction.OnExternalIdChange) { + mutableStateFlow.update { it.copy(externalId = action.value) } + } + + private fun handleStepChange(action: SavingsAccountAction.OnStepChange) { + mutableStateFlow.update { it.copy(currentStep = action.newIndex) } + } + + private fun handleSubmissionDatePick(action: SavingsAccountAction.OnSubmissionDatePick) { + mutableStateFlow.update { it.copy(showSubmissionDatePick = action.state) } + } + + private fun handleSubmissionDateChange(action: SavingsAccountAction.OnSubmissionDateChange) { + mutableStateFlow.update { it.copy(submissionDate = action.date) } + } + + private fun handleOnDetailsSubmit() { + mutableStateFlow.update { + it.copy( + externalIdError = null, + ) + } + val externalIdError = TextFieldsValidator.optionalStringValidator(state.externalId) + if (externalIdError != null) { + mutableStateFlow.update { it.copy(externalIdError = externalIdError) } + return + } else { + moveToNextStep() } } + private fun loadClientTemplate() = viewModelScope.launch { + val online = networkMonitor.isOnline.first() + if (online) { + getClientTemplateUseCase().collect { result -> + sendAction(SavingsAccountAction.Internal.OnReceivingClientTemplate(result)) + } + } else { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.NetworkError, + ) + } + } + } + + private fun handleRetry() { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + loadClientTemplate() + } + private fun moveToNextStep() { val current = state.currentStep if (current < state.totalSteps) { @@ -43,14 +162,34 @@ internal class SavingsAccountViewModel : } data class SavingsAccountState( + val clientId: Int, + val fieldOfficerIndex: Int = -1, + val fieldOfficerOptions: List = emptyList(), + val isOverLayLoadingActive: Boolean = false, + val savingsProductSelected: Int = -1, + val savingProductOptions: List = emptyList(), val currentStep: Int = 0, val totalSteps: Int = 4, val dialogState: DialogState? = null, + val externalId: String = "", + val externalIdError: StringResource? = null, + val screenState: ScreenState = ScreenState.Loading, + val submissionDate: String = DateHelper.getDateAsStringFromLong(Clock.System.now().toEpochMilliseconds()), + val showSubmissionDatePick: Boolean = false, ) { sealed interface DialogState { data class Error(val message: String) : DialogState - data object Loading : DialogState } + + sealed interface ScreenState { + data object Loading : ScreenState + data object Success : ScreenState + data object NetworkError : ScreenState + } + + val isDetailsNextEnabled = submissionDate.isNotEmpty() && + savingsProductSelected != -1 && + fieldOfficerIndex != -1 } sealed interface SavingsAccountEvent { @@ -63,4 +202,15 @@ sealed interface SavingsAccountAction { data object NextStep : SavingsAccountAction data object Finish : SavingsAccountAction data class OnStepChange(val newIndex: Int) : SavingsAccountAction + data class OnSubmissionDateChange(val date: String) : SavingsAccountAction + data class OnSubmissionDatePick(val state: Boolean) : SavingsAccountAction + data object OnDetailsSubmit : SavingsAccountAction + data class OnProductNameChange(val index: Int) : SavingsAccountAction + data class OnFieldOfficerChange(val index: Int) : SavingsAccountAction + data class OnExternalIdChange(val value: String) : SavingsAccountAction + data object Retry : SavingsAccountAction + + sealed interface Internal : SavingsAccountAction { + data class OnReceivingClientTemplate(val clientTemplate: DataState) : Internal + } } diff --git a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt index 317df67dad3..276020a2757 100644 --- a/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt +++ b/feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/pages/DetailsPage.kt @@ -10,26 +10,161 @@ 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_cancel +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_next +import androidclient.feature.savings.generated.resources.feature_savings_product_name +import androidclient.feature.savings.generated.resources.feature_savings_select +import androidclient.feature.savings.generated.resources.feature_savings_submission_date import androidclient.feature.savings.generated.resources.step_details 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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SelectableDates import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState 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.common.utils.DateHelper +import com.mifos.core.designsystem.component.MifosDatePickerTextField +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.datetime.Clock import org.jetbrains.compose.resources.stringResource +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun DetailsPage(onNext: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(stringResource(Res.string.step_details)) - Spacer(Modifier.height(8.dp)) - Button(onClick = onNext) { - Text(stringResource(Res.string.feature_savings_submit)) +fun DetailsPage( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit, + modifier: Modifier = Modifier, +) { + val submissionDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = Clock.System.now().toEpochMilliseconds(), + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= Clock.System.now().toEpochMilliseconds().minus(86_400_000L) + } + }, + ) + + if (state.showSubmissionDatePick) { + DatePickerDialog( + onDismissRequest = { + onAction(SavingsAccountAction.OnSubmissionDatePick(state = false)) + }, + confirmButton = { + TextButton( + onClick = { + onAction(SavingsAccountAction.OnSubmissionDatePick(state = false)) + submissionDatePickerState.selectedDateMillis?.let { + onAction( + SavingsAccountAction.OnSubmissionDateChange( + DateHelper.getDateAsStringFromLong(it), + ), + ) + } + }, + ) { Text(stringResource(Res.string.feature_savings_select)) } + }, + dismissButton = { + TextButton( + onClick = { + onAction(SavingsAccountAction.OnSubmissionDatePick(state = false)) + }, + ) { Text(stringResource(Res.string.feature_savings_cancel)) } + }, + ) { + DatePicker(state = submissionDatePickerState) + } + } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = modifier.weight(1f).verticalScroll(rememberScrollState()), + ) { + Text( + text = stringResource(Res.string.step_details), + style = MifosTypography.labelLargeEmphasized, + ) + Spacer(Modifier.height(DesignToken.padding.large)) + + MifosTextFieldDropdown( + value = if (state.savingsProductSelected == -1) { + "" + } else { + state.savingProductOptions[state.savingsProductSelected].name + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnProductNameChange(index)) + }, + options = state.savingProductOptions.map { + it.name + }, + label = stringResource(Res.string.feature_savings_product_name), + ) + MifosDatePickerTextField( + value = state.submissionDate, + label = stringResource(Res.string.feature_savings_submission_date), + openDatePicker = { + onAction(SavingsAccountAction.OnSubmissionDatePick(true)) + }, + ) + + Spacer(Modifier.height(DesignToken.padding.large)) + MifosTextFieldDropdown( + value = if (state.fieldOfficerIndex == -1) { + "" + } else { + state.fieldOfficerOptions[state.fieldOfficerIndex].displayName + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(SavingsAccountAction.OnFieldOfficerChange(index)) + }, + options = state.fieldOfficerOptions.map { + it.displayName + }, + label = stringResource(Res.string.feature_savings_field_officer), + ) + + MifosOutlinedTextField( + value = state.externalId, + onValueChange = { + onAction(SavingsAccountAction.OnExternalIdChange(it)) + }, + label = stringResource(Res.string.feature_savings_external_id), + config = MifosTextFieldConfig( + isError = state.externalIdError != null, + errorText = if (state.externalIdError != null) stringResource(state.externalIdError) else null, + ), + ) + Spacer(Modifier.height(DesignToken.padding.large)) } + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.feature_savings_back), + secondBtnText = stringResource(Res.string.feature_savings_next), + onFirstBtnClick = { onAction(SavingsAccountAction.NavigateBack) }, + onSecondBtnClick = { onAction(SavingsAccountAction.OnDetailsSubmit) }, + isSecondButtonEnabled = state.isDetailsNextEnabled, + modifier = Modifier.padding(top = DesignToken.padding.small), + ) } }