From c4d9a2c05793059baa24b7641a655572f4bdfce1 Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Sun, 7 Sep 2025 00:14:30 +0530 Subject: [PATCH 1/6] Initial changes --- .../AuthenticatedNavbarNavigationScreen.kt | 4 + .../authenticated/AuthenticatedNavbarRoute.kt | 2 + .../authenticated/AuthenticatedNavigation.kt | 2 + .../ClientApplyNewApplicationRoute.kt | 2 +- .../ClientApplyNewApplicationsScreen.kt | 4 +- .../client/navigation/ClientNavigation.kt | 3 +- .../values/feature_savings_strings.xml | 3 + .../mifos/feature/savings/di/SavingsModule.kt | 2 + .../savings/navigation/SavingsNavigation.kt | 6 +- .../savingsAccountv2/SavingsAccountRoute.kt | 19 ++- .../savingsAccountv2/SavingsAccountScreen.kt | 82 +++++++++-- .../SavingsAccountViewModel.kt | 119 +++++++++++++-- .../savingsAccountv2/pages/DetailsPage.kt | 138 +++++++++++++++++- 13 files changed, 346 insertions(+), 40 deletions(-) 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..81e57c22b2d 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..ada4cf91569 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..1d28cc78813 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/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..41181d4115e 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..b3f4cdb763b 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,9 @@ Go Back Savings Products Submit + 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..2153b975c35 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..619a05e0970 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..06757abe04a 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 @@ -15,13 +15,21 @@ 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.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.ui.components.MifosBreadcrumbNavBar +import com.mifos.core.ui.components.MifosErrorComponent +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,53 @@ 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 -> { + MifosErrorComponent( + isNetworkConnected = state.networkConnection, + isRetryEnabled = true, + onRetry = { onAction(SavingsAccountAction.Retry) } + ) + } + } + if (state.isOverLayLoadingActive) { + MifosProgressIndicatorOverlay() + } + } +} + +@Composable +private fun NewSavingsAccountDialog( + state: SavingsAccountState, + onAction: (SavingsAccountAction) -> Unit +) { + when(state.dialogState) { + is SavingsAccountState.DialogState.Error -> { + MifosErrorComponent( + isNetworkConnected = state.networkConnection, + isRetryEnabled = true, + onRetry = {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..1285d06d3bb 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,83 @@ */ package com.mifos.feature.savings.savingsAccountv2 +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.mifos.core.common.utils.DateHelper +import com.mifos.core.data.util.NetworkMonitor import com.mifos.core.ui.util.BaseViewModel +import com.mifos.core.ui.util.TextFieldsValidator 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, + val savedStateHandle: SavedStateHandle, +) : BaseViewModel( - initialState = SavingsAccountState(), + initialState = run { + SavingsAccountState(clientId = savedStateHandle.toRoute().clientId) + }, ) { + init { + observeNetwork() + } + 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) } } + 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 = null + if (externalIdError == null) { + moveToNextStep() + } else { + mutableStateFlow.update { + it.copy(externalIdError = externalIdError) + } + } + } + + private fun handleRetry() { + mutableStateFlow.update { + it.copy( + dialogState = null + ) + } + observeNetwork() + } private fun moveToNextStep() { val current = state.currentStep if (current < state.totalSteps) { @@ -40,17 +98,55 @@ internal class SavingsAccountViewModel : sendEvent(SavingsAccountEvent.Finish) } } + private fun observeNetwork() { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + mutableStateFlow.update { + it.copy(networkConnection = isConnected, + screenState = SavingsAccountState.ScreenState.Success) + } + if (isConnected) { + println("Is connected ") + } else { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.NetworkError, + ) + } + } + } + } + } + } data class SavingsAccountState( + val clientId: Int, + val networkConnection: Boolean = false, + val fieldOfficerIndex: Int = -1, + val isOverLayLoadingActive: Boolean = false, + val savingsProductSelected: Int = -1, 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() + && externalId.all { it.isLetterOrDigit() } +// && savingsProductSelected != -1 } sealed interface SavingsAccountEvent { @@ -63,4 +159,9 @@ 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 OnExternalIdChange(val value: String) : SavingsAccountAction + data object Retry: SavingsAccountAction } 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..425b13cc4b2 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,152 @@ 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_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_submission_date import androidclient.feature.savings.generated.resources.feature_savings_submit 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.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api 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(), + ) + + 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("Select") } + }, + dismissButton = { + TextButton( + onClick = { + onAction(SavingsAccountAction.OnSubmissionDatePick(state = false)) + }, + ) { Text("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 { + "Unit" + }, + onValueChanged = {}, + onOptionSelected = {index, value ->}, + options = emptyList(), + label = stringResource(Res.string.feature_savings_product_name) + ) + MifosTextFieldDropdown( + value = if (state.fieldOfficerIndex == -1) { + "" + } else { + "Unit" + }, + onValueChanged = {}, + onOptionSelected = { index, value -> + + }, + options = emptyList(), + label = stringResource(Res.string.feature_savings_field_officer), + ) + + MifosDatePickerTextField( + value = state.submissionDate, + label = stringResource(Res.string.feature_savings_submission_date), + openDatePicker = { + onAction(SavingsAccountAction.OnSubmissionDatePick(true)) + }, + ) + + Spacer(Modifier.height(DesignToken.padding.large)) + 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), + ) } } From 3d082e29b4c2ab06d99f42ed0ba2f10a934e3aaa Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Tue, 9 Sep 2025 13:31:24 +0530 Subject: [PATCH 2/6] feature(savings): new savings account, details step impl --- .../com/mifos/core/domain/di/UseCaseModule.kt | 2 + .../useCases/GetClientTemplateUseCase.kt | 14 +++ .../mifos/core/ui/util/TextFieldsValidator.kt | 7 ++ .../values/feature_savings_strings.xml | 2 + .../SavingsAccountViewModel.kt | 107 ++++++++++++++---- .../savingsAccountv2/pages/DetailsPage.kt | 51 +++++---- 6 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientTemplateUseCase.kt 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..16fabf6e77a --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientTemplateUseCase.kt @@ -0,0 +1,14 @@ +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() + } +} \ No newline at end of file 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/savings/src/commonMain/composeResources/values/feature_savings_strings.xml b/feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml index b3f4cdb763b..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,8 @@ Go Back Savings Products Submit + Select + ExternalId must be unique Submission Date Next Back 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 1285d06d3bb..2f5e3ffcf7e 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 @@ -12,17 +12,23 @@ 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.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( @@ -46,9 +52,47 @@ internal class SavingsAccountViewModel ( 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) } } @@ -64,28 +108,37 @@ internal class SavingsAccountViewModel ( private fun handleSubmissionDateChange(action: SavingsAccountAction.OnSubmissionDateChange) { mutableStateFlow.update { it.copy(submissionDate = action.date) } } + private fun handleOnDetailsSubmit() { - mutableStateFlow.update { it.copy( - externalIdError = null - ) } - val externalIdError = null - if (externalIdError == null) { - moveToNextStep() + mutableStateFlow.update { + it.copy( + externalIdError = null, + ) + } + val externalIdError = TextFieldsValidator.optionalStringValidator(state.externalId) + if (externalIdError != null) { + mutableStateFlow.update { it.copy(externalIdError = externalIdError) } + return } else { - mutableStateFlow.update { - it.copy(externalIdError = externalIdError) - } + moveToNextStep() + } + } + + private fun loadClientTemplate() = viewModelScope.launch { + getClientTemplateUseCase().collect { result -> + sendAction(SavingsAccountAction.Internal.OnReceivingClientTemplate(result)) } } private fun handleRetry() { mutableStateFlow.update { it.copy( - dialogState = null + dialogState = null, ) } observeNetwork() } + private fun moveToNextStep() { val current = state.currentStep if (current < state.totalSteps) { @@ -98,15 +151,18 @@ internal class SavingsAccountViewModel ( sendEvent(SavingsAccountEvent.Finish) } } + private fun observeNetwork() { viewModelScope.launch { networkMonitor.isOnline.collect { isConnected -> mutableStateFlow.update { - it.copy(networkConnection = isConnected, - screenState = SavingsAccountState.ScreenState.Success) + it.copy( + networkConnection = isConnected, + screenState = SavingsAccountState.ScreenState.Success, + ) } if (isConnected) { - println("Is connected ") + loadClientTemplate() } else { mutableStateFlow.update { it.copy( @@ -124,8 +180,10 @@ data class SavingsAccountState( val clientId: Int, val networkConnection: Boolean = false, 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, @@ -133,20 +191,21 @@ data class SavingsAccountState( val externalIdError: StringResource? = null, val screenState: ScreenState = ScreenState.Loading, val submissionDate: String = DateHelper.getDateAsStringFromLong(Clock.System.now().toEpochMilliseconds()), - val showSubmissionDatePick: Boolean = false + val showSubmissionDatePick: Boolean = false, ) { sealed interface DialogState { data class Error(val message: String) : DialogState } sealed interface ScreenState { - data object Loading: ScreenState - data object Success: ScreenState - data object NetworkError: ScreenState + data object Loading : ScreenState + data object Success : ScreenState + data object NetworkError : ScreenState } + val isDetailsNextEnabled = submissionDate.isNotEmpty() - && externalId.all { it.isLetterOrDigit() } -// && savingsProductSelected != -1 + && savingsProductSelected != -1 + && fieldOfficerIndex != -1 } sealed interface SavingsAccountEvent { @@ -162,6 +221,12 @@ sealed interface 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 + 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 425b13cc4b2..3661f49ccff 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 @@ -16,8 +16,8 @@ import androidclient.feature.savings.generated.resources.feature_savings_externa 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.feature_savings_submit import androidclient.feature.savings.generated.resources.step_details import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -26,17 +26,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button 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 @@ -59,6 +57,11 @@ fun DetailsPage( ) { 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) { @@ -78,14 +81,14 @@ fun DetailsPage( ) } }, - ) { Text("Select") } + ) { Text(stringResource(Res.string.feature_savings_select)) } }, dismissButton = { TextButton( onClick = { onAction(SavingsAccountAction.OnSubmissionDatePick(state = false)) }, - ) { Text("Cancel") } + ) { Text(stringResource(Res.string.feature_savings_cancel)) } }, ) { DatePicker(state = submissionDatePickerState) @@ -106,36 +109,42 @@ fun DetailsPage( value = if (state.savingsProductSelected == -1) { "" } else { - "Unit" + state.savingProductOptions[state.savingsProductSelected].name }, onValueChanged = {}, - onOptionSelected = {index, value ->}, - options = emptyList(), + 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 { - "Unit" + state.fieldOfficerOptions[state.fieldOfficerIndex].displayName }, onValueChanged = {}, onOptionSelected = { index, value -> - + onAction(SavingsAccountAction.OnFieldOfficerChange(index)) }, - options = emptyList(), - label = stringResource(Res.string.feature_savings_field_officer), - ) - - MifosDatePickerTextField( - value = state.submissionDate, - label = stringResource(Res.string.feature_savings_submission_date), - openDatePicker = { - onAction(SavingsAccountAction.OnSubmissionDatePick(true)) + options = state.fieldOfficerOptions.map { + it.displayName }, + label = stringResource(Res.string.feature_savings_field_officer), ) - Spacer(Modifier.height(DesignToken.padding.large)) MifosOutlinedTextField( value = state.externalId, onValueChange = { From 6778a6fc72c271960e6434ebe811c0df484d5c3b Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Tue, 9 Sep 2025 13:39:52 +0530 Subject: [PATCH 3/6] feature(savings): new savings account, details step impl, fix static checks --- .../AuthenticatedNavbarNavigationScreen.kt | 4 ++-- .../authenticated/AuthenticatedNavbarRoute.kt | 2 +- .../authenticated/AuthenticatedNavigation.kt | 2 +- .../domain/useCases/GetClientTemplateUseCase.kt | 15 ++++++++++++--- .../feature/client/navigation/ClientNavigation.kt | 2 +- .../savings/navigation/SavingsNavigation.kt | 2 +- .../savingsAccountv2/SavingsAccountRoute.kt | 4 ++-- .../savingsAccountv2/SavingsAccountScreen.kt | 14 +++++++------- .../savingsAccountv2/SavingsAccountViewModel.kt | 9 +++------ .../savings/savingsAccountv2/pages/DetailsPage.kt | 6 +++--- 10 files changed, 33 insertions(+), 27 deletions(-) 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 81e57c22b2d..78fceaedb2b 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarNavigationScreen.kt @@ -180,7 +180,7 @@ internal fun AuthenticatedNavbarNavigationScreen( navigateToDocumentScreen = navigateToDocumentScreen, navigateToNoteScreen = navigateToNoteScreen, navigateToNewLoanAccountScreen = navigateToNewLoanAccountScreen, - navigateToNewSavingsAccountScreen = navigateToNewSavingsAccountScreen + navigateToNewSavingsAccountScreen = navigateToNewSavingsAccountScreen, ) } @@ -436,7 +436,7 @@ internal fun AuthenticatedNavbarNavigationScreenContent( hasDatatables = navController::navigateDataTableList, onDocumentClicked = navigateToDocumentScreen, navigateToNewLoanAccount = navigateToNewLoanAccountScreen, - navigateToNewSavingsAccount = navigateToNewSavingsAccountScreen + 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 ada4cf91569..5aa5bd7f3f3 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavbarRoute.kt @@ -35,7 +35,7 @@ internal fun NavGraphBuilder.authenticatedNavbarGraph( navigateToDocumentScreen = navigateToDocumentScreen, navigateToNoteScreen = navigateToNoteScreen, navigateToNewLoanAccountScreen = navigateToNewLoanAccountScreen, - navigateToNewSavingsAccountScreen = navigateToNewSavingsAccountScreen + 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 1d28cc78813..928abab4538 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticated/AuthenticatedNavigation.kt @@ -62,7 +62,7 @@ internal fun NavGraphBuilder.authenticatedGraph( navigateToDocumentScreen = navController::navigateToDocumentListScreen, navigateToNoteScreen = navController::navigateToNoteScreen, navigateToNewLoanAccountScreen = navController::navigateToNewLoanAccountRoute, - navigateToNewSavingsAccountScreen = navController::navigateToSavingsAccountRoute + navigateToNewSavingsAccountScreen = navController::navigateToSavingsAccountRoute, ) checkerInboxTaskNavGraph(navController) 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 index 16fabf6e77a..5377b343ea2 100644 --- 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 @@ -1,3 +1,12 @@ +/* + * 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 @@ -6,9 +15,9 @@ import com.mifos.room.entities.templates.clients.ClientsTemplateEntity import kotlinx.coroutines.flow.Flow class GetClientTemplateUseCase( - private val newClientRepository: CreateNewClientRepository + private val newClientRepository: CreateNewClientRepository, ) { - operator fun invoke() : Flow> { + operator fun invoke(): Flow> { return newClientRepository.clientTemplate() } -} \ No newline at end of file +} 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 41181d4115e..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,7 +91,7 @@ fun NavGraphBuilder.clientNavGraph( hasDatatables: KFunction4, Any?, Int, MutableList>, Unit>, onDocumentClicked: (Int, String) -> Unit, navigateToNewLoanAccount: (Int) -> Unit, - navigateToNewSavingsAccount: (Int) -> Unit + navigateToNewSavingsAccount: (Int) -> Unit, ) { navigation( startDestination = ClientListScreenRoute, 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 2153b975c35..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 @@ -88,7 +88,7 @@ fun NavGraphBuilder.savingsNavGraph( savingsAccountDestination( navController = navController, onNavigateBack = onBackPressed, - onFinish = 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 619a05e0970..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 @@ -16,7 +16,7 @@ import kotlinx.serialization.Serializable @Serializable data class SavingsAccountRoute( - val clientId: Int = -1 + val clientId: Int = -1, ) fun NavGraphBuilder.savingsAccountDestination( @@ -28,7 +28,7 @@ fun NavGraphBuilder.savingsAccountDestination( SavingsAccountScreen( onNavigateBack = onNavigateBack, onFinish = onFinish, - navController = navController + navController = navController, ) } } 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 06757abe04a..4cd0b1405a8 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 @@ -59,7 +59,7 @@ internal fun SavingsAccountScreen( NewSavingsAccountDialog( state = state, - onAction = { viewModel.trySendAction(it) } + onAction = { viewModel.trySendAction(it) }, ) SavingsAccountScaffold( @@ -81,7 +81,7 @@ private fun SavingsAccountScaffold( Step(stringResource(Res.string.step_details)) { DetailsPage( state = state, - onAction = onAction + onAction = onAction, ) }, Step(stringResource(Res.string.step_terms)) { @@ -106,7 +106,7 @@ private fun SavingsAccountScaffold( onBackPressed = { onAction(SavingsAccountAction.NavigateBack) }, modifier = modifier, ) { paddingValues -> - when(state.screenState) { + when (state.screenState) { is SavingsAccountState.ScreenState.Loading -> MifosProgressIndicator() is SavingsAccountState.ScreenState.Success -> { Column( @@ -130,7 +130,7 @@ private fun SavingsAccountScaffold( MifosErrorComponent( isNetworkConnected = state.networkConnection, isRetryEnabled = true, - onRetry = { onAction(SavingsAccountAction.Retry) } + onRetry = { onAction(SavingsAccountAction.Retry) }, ) } } @@ -143,14 +143,14 @@ private fun SavingsAccountScaffold( @Composable private fun NewSavingsAccountDialog( state: SavingsAccountState, - onAction: (SavingsAccountAction) -> Unit + onAction: (SavingsAccountAction) -> Unit, ) { - when(state.dialogState) { + when (state.dialogState) { is SavingsAccountState.DialogState.Error -> { MifosErrorComponent( isNetworkConnected = state.networkConnection, isRetryEnabled = true, - onRetry = {onAction(SavingsAccountAction.Retry)} + onRetry = { 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 2f5e3ffcf7e..a2e93572306 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 @@ -58,7 +58,6 @@ internal class SavingsAccountViewModel( } } - private fun handleFieldOfficerChange(action: SavingsAccountAction.OnFieldOfficerChange) { mutableStateFlow.update { it.copy(fieldOfficerIndex = action.index) } } @@ -90,7 +89,6 @@ internal class SavingsAccountViewModel( private fun handleOnProductNameChange(action: SavingsAccountAction.OnProductNameChange) { mutableStateFlow.update { it.copy(savingsProductSelected = action.index) } - } private fun handleExternalIdChange(action: SavingsAccountAction.OnExternalIdChange) { @@ -173,7 +171,6 @@ internal class SavingsAccountViewModel( } } } - } data class SavingsAccountState( @@ -203,9 +200,9 @@ data class SavingsAccountState( data object NetworkError : ScreenState } - val isDetailsNextEnabled = submissionDate.isNotEmpty() - && savingsProductSelected != -1 - && fieldOfficerIndex != -1 + val isDetailsNextEnabled = submissionDate.isNotEmpty() && + savingsProductSelected != -1 && + fieldOfficerIndex != -1 } sealed interface SavingsAccountEvent { 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 3661f49ccff..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 @@ -112,13 +112,13 @@ fun DetailsPage( state.savingProductOptions[state.savingsProductSelected].name }, onValueChanged = {}, - onOptionSelected = {index, value -> + onOptionSelected = { index, value -> onAction(SavingsAccountAction.OnProductNameChange(index)) }, - options = state.savingProductOptions.map{ + options = state.savingProductOptions.map { it.name }, - label = stringResource(Res.string.feature_savings_product_name) + label = stringResource(Res.string.feature_savings_product_name), ) MifosDatePickerTextField( value = state.submissionDate, From 6f7473da8813cd23c81f21114fba7f6427f98188 Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Tue, 9 Sep 2025 19:41:49 +0530 Subject: [PATCH 4/6] feature(savings): refactor usage of network monitor --- .../SavingsAccountViewModel.kt | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) 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 a2e93572306..a9208997a45 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 @@ -21,6 +21,7 @@ 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 @@ -38,7 +39,7 @@ internal class SavingsAccountViewModel( ) { init { - observeNetwork() + loadClientTemplate() } override fun handleAction(action: SavingsAccountAction) { @@ -123,8 +124,18 @@ internal class SavingsAccountViewModel( } private fun loadClientTemplate() = viewModelScope.launch { - getClientTemplateUseCase().collect { result -> - sendAction(SavingsAccountAction.Internal.OnReceivingClientTemplate(result)) + val online = networkMonitor.isOnline.first() + mutableStateFlow.update { it.copy(networkConnection = online) } + if (online) { + getClientTemplateUseCase().collect { result -> + sendAction(SavingsAccountAction.Internal.OnReceivingClientTemplate(result)) + } + } else { + mutableStateFlow.update { + it.copy( + screenState = SavingsAccountState.ScreenState.NetworkError, + ) + } } } @@ -134,7 +145,7 @@ internal class SavingsAccountViewModel( dialogState = null, ) } - observeNetwork() + loadClientTemplate() } private fun moveToNextStep() { @@ -149,28 +160,6 @@ internal class SavingsAccountViewModel( sendEvent(SavingsAccountEvent.Finish) } } - - private fun observeNetwork() { - viewModelScope.launch { - networkMonitor.isOnline.collect { isConnected -> - mutableStateFlow.update { - it.copy( - networkConnection = isConnected, - screenState = SavingsAccountState.ScreenState.Success, - ) - } - if (isConnected) { - loadClientTemplate() - } else { - mutableStateFlow.update { - it.copy( - screenState = SavingsAccountState.ScreenState.NetworkError, - ) - } - } - } - } - } } data class SavingsAccountState( From 1a7fb12ded318abf3e129645eef47a3166368501 Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Tue, 9 Sep 2025 22:18:57 +0530 Subject: [PATCH 5/6] feature(savings): made requested changes --- .../savingsAccountv2/SavingsAccountScreen.kt | 18 ++++++------------ .../SavingsAccountViewModel.kt | 2 -- 2 files changed, 6 insertions(+), 14 deletions(-) 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 4cd0b1405a8..c86dcbcfa34 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,6 +11,7 @@ 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 @@ -23,11 +24,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel 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.MifosErrorComponent import com.mifos.core.ui.components.MifosProgressIndicator import com.mifos.core.ui.components.MifosProgressIndicatorOverlay import com.mifos.core.ui.components.MifosStepper @@ -59,7 +59,6 @@ internal fun SavingsAccountScreen( NewSavingsAccountDialog( state = state, - onAction = { viewModel.trySendAction(it) }, ) SavingsAccountScaffold( @@ -127,10 +126,8 @@ private fun SavingsAccountScaffold( } } is SavingsAccountState.ScreenState.NetworkError -> { - MifosErrorComponent( - isNetworkConnected = state.networkConnection, - isRetryEnabled = true, - onRetry = { onAction(SavingsAccountAction.Retry) }, + MifosSweetError( + message = stringResource(Res.string.feature_savings_error_not_connected_internet), ) } } @@ -143,14 +140,11 @@ private fun SavingsAccountScaffold( @Composable private fun NewSavingsAccountDialog( state: SavingsAccountState, - onAction: (SavingsAccountAction) -> Unit, ) { when (state.dialogState) { is SavingsAccountState.DialogState.Error -> { - MifosErrorComponent( - isNetworkConnected = state.networkConnection, - isRetryEnabled = true, - onRetry = { onAction(SavingsAccountAction.Retry) }, + MifosSweetError( + message = state.dialogState.message, ) } 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 a9208997a45..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 @@ -125,7 +125,6 @@ internal class SavingsAccountViewModel( private fun loadClientTemplate() = viewModelScope.launch { val online = networkMonitor.isOnline.first() - mutableStateFlow.update { it.copy(networkConnection = online) } if (online) { getClientTemplateUseCase().collect { result -> sendAction(SavingsAccountAction.Internal.OnReceivingClientTemplate(result)) @@ -164,7 +163,6 @@ internal class SavingsAccountViewModel( data class SavingsAccountState( val clientId: Int, - val networkConnection: Boolean = false, val fieldOfficerIndex: Int = -1, val fieldOfficerOptions: List = emptyList(), val isOverLayLoadingActive: Boolean = false, From c147ccaa5919abd85120eeec2012475c0a2abacd Mon Sep 17 00:00:00 2001 From: Samarth Chaplot Date: Tue, 9 Sep 2025 23:17:42 +0530 Subject: [PATCH 6/6] updated onClick listner of error components --- .../feature/savings/savingsAccountv2/SavingsAccountScreen.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 c86dcbcfa34..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 @@ -59,6 +59,7 @@ internal fun SavingsAccountScreen( NewSavingsAccountDialog( state = state, + onAction = { viewModel.trySendAction(it) }, ) SavingsAccountScaffold( @@ -128,6 +129,7 @@ private fun SavingsAccountScaffold( is SavingsAccountState.ScreenState.NetworkError -> { MifosSweetError( message = stringResource(Res.string.feature_savings_error_not_connected_internet), + onclick = { onAction(SavingsAccountAction.Retry) }, ) } } @@ -140,11 +142,13 @@ private fun SavingsAccountScaffold( @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