From 3ea74004f654f1251405bce3e542dca6fd32366d Mon Sep 17 00:00:00 2001 From: Nagarjuna Date: Fri, 28 Feb 2025 18:44:32 +0530 Subject: [PATCH] feat(feature:laon): migrated to CMP --- .../demoDebugRuntimeClasspath.txt | 24 +- .../demoReleaseRuntimeClasspath.txt | 24 +- .../prodDebugRuntimeClasspath.txt | 24 +- .../prodReleaseRuntimeClasspath.txt | 24 +- cmp-navigation/build.gradle.kts | 2 +- .../kotlin/cmp/navigation/di/KoinModules.kt | 2 + .../navigation/navigation/FeatureNavHost.kt | 22 +- .../core/common/FormatAmount.android.kt | 17 + .../mifos/mobile/core/common/DateHelper.kt | 23 + .../mifos/mobile/core/common/FormatAmount.kt | 12 + .../core/common/FormatAmount.desktop.kt | 17 + .../mobile/core/common/FormatAmount.js.kt | 14 + .../mobile/core/common/FormatAmount.native.kt | 23 + .../mobile/core/common/FormatAmount.wasmJs.kt | 17 + .../data/repositoryImpl/LoanRepositoryImp.kt | 12 +- .../accounts/loan/LoanWithAssociations.kt | 2 +- .../entity/accounts/loan/LoanWithdraw.kt | 2 + .../loan/calendardata/CalendarData.kt | 4 +- .../viewmodel/LoanAccountViewmodel.kt | 7 +- feature/loan/build.gradle.kts | 22 +- .../ic_assignment_turned_in_black_24dp.xml | 0 .../composeResources}/drawable/ic_charges.xml | 0 .../drawable/ic_check_circle_green_24px.xml | 0 .../drawable/ic_compare_arrows_black_24dp.xml | 0 .../drawable/ic_edit_black_24dp.xml | 0 .../drawable/ic_error_black_24dp.xml | 0 .../drawable/ic_local_atm_black_24dp.xml | 0 .../drawable/ic_qrcode_scan.xml | 0 .../drawable/ic_report_problem_red_24px.xml | 0 .../drawable/ic_surveys_48px.xml | 0 .../composeResources}/values/strings.xml | 0 .../mobile/feature/loan/di/LoanModule.kt | 31 ++ .../loanAccount/LoanAccountDetailContent.kt | 137 ++--- .../loanAccount/LoanAccountDetailScreen.kt | 196 ++++++++ .../loanAccount/LoanAccountDetailTopBar.kt | 26 +- .../LoanAccountsDetailViewModel.kt | 248 +++++++++ .../LoanApplicationContent.kt | 200 ++++---- .../LoanApplicationScreen.kt | 184 +++++++ .../LoanApplicationViewModel.kt | 471 ++++++++++++++++++ .../LoanAccountSummaryScreen.kt | 189 ++++--- .../LoanAccountSummaryViewModel.kt | 137 +++++ .../LoanAccountTransactionScreen.kt | 157 +++--- .../LoanAccountTransactionViewModel.kt | 135 +++++ .../LoanAccountWithdrawScreen.kt | 191 +++++++ .../LoanAccountWithdrawViewModel.kt | 151 ++++++ .../LoanRepaymentScheduleScreen.kt | 183 +++---- .../LoanRepaymentScheduleViewModel.kt | 113 +++++ .../ReviewLoanApplicationContent.kt | 65 ++- .../loanReview/ReviewLoanApplicationScreen.kt | 140 ++++++ .../ReviewLoanApplicationViewModel.kt | 180 +++++++ .../feature/loan/navigation/LoanNavGraph.kt | 89 ++-- .../feature/loan/navigation/LoanNavigation.kt | 20 +- feature/loan/src/main/AndroidManifest.xml | 13 - .../loanAccount/LoanAccountDetailScreen.kt | 179 ------- .../LoanAccountsDetailViewModel.kt | 88 ---- .../LoanApplicationScreen.kt | 280 ----------- .../LoanApplicationViewModel.kt | 277 ---------- .../LoanAccountSummaryViewModel.kt | 54 -- .../LoanAccountTransactionViewModel.kt | 58 --- .../LoanAccountWithdrawScreen.kt | 208 -------- .../LoanAccountWithdrawViewModel.kt | 81 --- .../LoanRepaymentScheduleViewModel.kt | 73 --- .../loanReview/ReviewLoanApplicationScreen.kt | 173 ------- .../ReviewLoanApplicationViewModel.kt | 124 ----- feature/loan/src/main/res/values/colors.xml | 83 --- .../viewmodel/SavingsAccountViewmodel.kt | 4 +- .../viewmodel/ShareAccountViewModel.kt | 7 +- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 2 +- 69 files changed, 2987 insertions(+), 2256 deletions(-) create mode 100644 core/common/src/androidMain/kotlin/org/mifos/mobile/core/common/FormatAmount.android.kt create mode 100644 core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/FormatAmount.kt create mode 100644 core/common/src/desktopMain/kotlin/org/mifos/mobile/core/common/FormatAmount.desktop.kt create mode 100644 core/common/src/jsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.js.kt create mode 100644 core/common/src/nativeMain/kotlin/org/mifos/mobile/core/common/FormatAmount.native.kt create mode 100644 core/common/src/wasmJsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.wasmJs.kt rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_assignment_turned_in_black_24dp.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_charges.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_check_circle_green_24px.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_compare_arrows_black_24dp.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_edit_black_24dp.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_error_black_24dp.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_local_atm_black_24dp.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_qrcode_scan.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_report_problem_red_24px.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/drawable/ic_surveys_48px.xml (100%) rename feature/loan/src/{main/res => commonMain/composeResources}/values/strings.xml (100%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/di/LoanModule.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt (58%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt (78%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt (50%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt (55%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt (50%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt (55%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt (66%) create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt create mode 100644 feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt (75%) rename feature/loan/src/{main/java => commonMain/kotlin}/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt (82%) delete mode 100644 feature/loan/src/main/AndroidManifest.xml delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt delete mode 100644 feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt delete mode 100644 feature/loan/src/main/res/values/colors.xml diff --git a/cmp-android/dependencies/demoDebugRuntimeClasspath.txt b/cmp-android/dependencies/demoDebugRuntimeClasspath.txt index dd39549c1..e4e29387b 100644 --- a/cmp-android/dependencies/demoDebugRuntimeClasspath.txt +++ b/cmp-android/dependencies/demoDebugRuntimeClasspath.txt @@ -215,20 +215,20 @@ io.github.vinceglb:filekit-compose-android:0.8.7 io.github.vinceglb:filekit-compose:0.8.7 io.github.vinceglb:filekit-core-android:0.8.7 io.github.vinceglb:filekit-core:0.8.7 -io.insert-koin:koin-android:4.0.1-RC1 -io.insert-koin:koin-androidx-compose:4.0.1-RC1 -io.insert-koin:koin-androidx-navigation:4.0.1-RC1 +io.insert-koin:koin-android:4.0.2 +io.insert-koin:koin-androidx-compose:4.0.2 +io.insert-koin:koin-androidx-navigation:4.0.2 io.insert-koin:koin-annotations-jvm:1.4.0-RC4 io.insert-koin:koin-annotations:1.4.0-RC4 -io.insert-koin:koin-bom:4.0.1-RC1 -io.insert-koin:koin-compose-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel:4.0.1-RC1 -io.insert-koin:koin-compose:4.0.1-RC1 -io.insert-koin:koin-core-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel:4.0.1-RC1 -io.insert-koin:koin-core:4.0.1-RC1 +io.insert-koin:koin-bom:4.0.2 +io.insert-koin:koin-compose-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel:4.0.2 +io.insert-koin:koin-compose:4.0.2 +io.insert-koin:koin-core-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel:4.0.2 +io.insert-koin:koin-core:4.0.2 io.ktor:ktor-client-auth-jvm:3.0.3 io.ktor:ktor-client-auth:3.0.3 io.ktor:ktor-client-content-negotiation-jvm:3.0.3 diff --git a/cmp-android/dependencies/demoReleaseRuntimeClasspath.txt b/cmp-android/dependencies/demoReleaseRuntimeClasspath.txt index 3574903eb..aa1c78305 100644 --- a/cmp-android/dependencies/demoReleaseRuntimeClasspath.txt +++ b/cmp-android/dependencies/demoReleaseRuntimeClasspath.txt @@ -211,20 +211,20 @@ io.github.vinceglb:filekit-compose-android:0.8.7 io.github.vinceglb:filekit-compose:0.8.7 io.github.vinceglb:filekit-core-android:0.8.7 io.github.vinceglb:filekit-core:0.8.7 -io.insert-koin:koin-android:4.0.1-RC1 -io.insert-koin:koin-androidx-compose:4.0.1-RC1 -io.insert-koin:koin-androidx-navigation:4.0.1-RC1 +io.insert-koin:koin-android:4.0.2 +io.insert-koin:koin-androidx-compose:4.0.2 +io.insert-koin:koin-androidx-navigation:4.0.2 io.insert-koin:koin-annotations-jvm:1.4.0-RC4 io.insert-koin:koin-annotations:1.4.0-RC4 -io.insert-koin:koin-bom:4.0.1-RC1 -io.insert-koin:koin-compose-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel:4.0.1-RC1 -io.insert-koin:koin-compose:4.0.1-RC1 -io.insert-koin:koin-core-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel:4.0.1-RC1 -io.insert-koin:koin-core:4.0.1-RC1 +io.insert-koin:koin-bom:4.0.2 +io.insert-koin:koin-compose-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel:4.0.2 +io.insert-koin:koin-compose:4.0.2 +io.insert-koin:koin-core-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel:4.0.2 +io.insert-koin:koin-core:4.0.2 io.ktor:ktor-client-auth-jvm:3.0.3 io.ktor:ktor-client-auth:3.0.3 io.ktor:ktor-client-content-negotiation-jvm:3.0.3 diff --git a/cmp-android/dependencies/prodDebugRuntimeClasspath.txt b/cmp-android/dependencies/prodDebugRuntimeClasspath.txt index dd39549c1..e4e29387b 100644 --- a/cmp-android/dependencies/prodDebugRuntimeClasspath.txt +++ b/cmp-android/dependencies/prodDebugRuntimeClasspath.txt @@ -215,20 +215,20 @@ io.github.vinceglb:filekit-compose-android:0.8.7 io.github.vinceglb:filekit-compose:0.8.7 io.github.vinceglb:filekit-core-android:0.8.7 io.github.vinceglb:filekit-core:0.8.7 -io.insert-koin:koin-android:4.0.1-RC1 -io.insert-koin:koin-androidx-compose:4.0.1-RC1 -io.insert-koin:koin-androidx-navigation:4.0.1-RC1 +io.insert-koin:koin-android:4.0.2 +io.insert-koin:koin-androidx-compose:4.0.2 +io.insert-koin:koin-androidx-navigation:4.0.2 io.insert-koin:koin-annotations-jvm:1.4.0-RC4 io.insert-koin:koin-annotations:1.4.0-RC4 -io.insert-koin:koin-bom:4.0.1-RC1 -io.insert-koin:koin-compose-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel:4.0.1-RC1 -io.insert-koin:koin-compose:4.0.1-RC1 -io.insert-koin:koin-core-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel:4.0.1-RC1 -io.insert-koin:koin-core:4.0.1-RC1 +io.insert-koin:koin-bom:4.0.2 +io.insert-koin:koin-compose-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel:4.0.2 +io.insert-koin:koin-compose:4.0.2 +io.insert-koin:koin-core-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel:4.0.2 +io.insert-koin:koin-core:4.0.2 io.ktor:ktor-client-auth-jvm:3.0.3 io.ktor:ktor-client-auth:3.0.3 io.ktor:ktor-client-content-negotiation-jvm:3.0.3 diff --git a/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt b/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt index 3574903eb..aa1c78305 100644 --- a/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt +++ b/cmp-android/dependencies/prodReleaseRuntimeClasspath.txt @@ -211,20 +211,20 @@ io.github.vinceglb:filekit-compose-android:0.8.7 io.github.vinceglb:filekit-compose:0.8.7 io.github.vinceglb:filekit-core-android:0.8.7 io.github.vinceglb:filekit-core:0.8.7 -io.insert-koin:koin-android:4.0.1-RC1 -io.insert-koin:koin-androidx-compose:4.0.1-RC1 -io.insert-koin:koin-androidx-navigation:4.0.1-RC1 +io.insert-koin:koin-android:4.0.2 +io.insert-koin:koin-androidx-compose:4.0.2 +io.insert-koin:koin-androidx-navigation:4.0.2 io.insert-koin:koin-annotations-jvm:1.4.0-RC4 io.insert-koin:koin-annotations:1.4.0-RC4 -io.insert-koin:koin-bom:4.0.1-RC1 -io.insert-koin:koin-compose-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-compose-viewmodel:4.0.1-RC1 -io.insert-koin:koin-compose:4.0.1-RC1 -io.insert-koin:koin-core-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel-jvm:4.0.1-RC1 -io.insert-koin:koin-core-viewmodel:4.0.1-RC1 -io.insert-koin:koin-core:4.0.1-RC1 +io.insert-koin:koin-bom:4.0.2 +io.insert-koin:koin-compose-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel-jvm:4.0.2 +io.insert-koin:koin-compose-viewmodel:4.0.2 +io.insert-koin:koin-compose:4.0.2 +io.insert-koin:koin-core-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel-jvm:4.0.2 +io.insert-koin:koin-core-viewmodel:4.0.2 +io.insert-koin:koin-core:4.0.2 io.ktor:ktor-client-auth-jvm:3.0.3 io.ktor:ktor-client-auth:3.0.3 io.ktor:ktor-client-content-negotiation-jvm:3.0.3 diff --git a/cmp-navigation/build.gradle.kts b/cmp-navigation/build.gradle.kts index 911eabc43..5abc4b348 100644 --- a/cmp-navigation/build.gradle.kts +++ b/cmp-navigation/build.gradle.kts @@ -17,7 +17,7 @@ plugins { kotlin { sourceSets { commonMain.dependencies { - + implementation(projects.feature.loan) implementation(projects.feature.auth) implementation(projects.feature.home) implementation(projects.feature.accounts) diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt index 6cda9af51..68801718f 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt @@ -20,6 +20,7 @@ import org.mifos.mobile.core.network.di.NetworkModule import org.mifos.mobile.feature.accounts.di.accountsModule import org.mifos.mobile.feature.auth.di.AuthModule import org.mifos.mobile.feature.home.di.HomeModule +import org.mifos.mobile.feature.loan.di.LoanModule import org.mifos.mobile.feature.loanaccount.di.loanAccountModule import org.mifos.mobile.feature.savingsaccount.di.savingsAccountModule import org.mifos.mobile.feature.shareaccount.di.shareAccountModule @@ -48,6 +49,7 @@ object KoinModules { savingsAccountModule, loanAccountModule, shareAccountModule, + LoanModule, ) } private val LibraryModule = module { diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt index 62d7effe0..23ceab9c8 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt @@ -24,6 +24,9 @@ import org.mifos.mobile.feature.accounts.navigation.navigateToAccountsScreen import org.mifos.mobile.feature.home.navigation.HomeDestinations import org.mifos.mobile.feature.home.navigation.HomeNavigation import org.mifos.mobile.feature.home.navigation.homeNavGraph +import org.mifos.mobile.feature.loan.navigation.loanNavGraph +import org.mifos.mobile.feature.loan.navigation.navigateToLoanApplication +import org.mifos.mobile.feature.loan.navigation.navigateToLoanDetailScreen @Composable internal fun FeatureNavHost( @@ -45,12 +48,27 @@ internal fun FeatureNavHost( accountsNavGraph( navController = appState.navController, - navigateToLoanApplicationScreen = { }, + navigateToLoanApplicationScreen = appState.navController::navigateToLoanApplication, navigateToSavingsApplicationScreen = { }, - navigateToAccountDetail = { _, _ -> }, + navigateToAccountDetail = { accountType, id -> + when (accountType) { + AccountType.SAVINGS -> { } + AccountType.LOAN -> + appState.navController.navigateToLoanDetailScreen(loanId = id) + AccountType.SHARE -> { } + } + }, ) aboutUsNavGraph(navController = appState.navController, navigateToOssLicense = { }) + + loanNavGraph( + navController = appState.navController, + viewQr = { }, + viewGuarantor = { }, + viewCharges = { }, + makePayment = { _, _, _ -> }, + ) } } diff --git a/core/common/src/androidMain/kotlin/org/mifos/mobile/core/common/FormatAmount.android.kt b/core/common/src/androidMain/kotlin/org/mifos/mobile/core/common/FormatAmount.android.kt new file mode 100644 index 000000000..4190e69a2 --- /dev/null +++ b/core/common/src/androidMain/kotlin/org/mifos/mobile/core/common/FormatAmount.android.kt @@ -0,0 +1,17 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +import java.text.DecimalFormat + +actual fun formatAmount(amount: Double): String { + val formatter = DecimalFormat("#,##0.00") + return formatter.format(amount) +} diff --git a/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/DateHelper.kt b/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/DateHelper.kt index 87f6073be..0403f5c0f 100644 --- a/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/DateHelper.kt +++ b/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/DateHelper.kt @@ -19,6 +19,7 @@ import kotlinx.datetime.format import kotlinx.datetime.format.FormatStringsInDatetimeFormats import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.toLocalDateTime +import org.mifos.mobile.core.common.FileUtils.Companion.logger @OptIn(FormatStringsInDatetimeFormats::class) object DateHelper { @@ -51,6 +52,7 @@ object DateHelper { * @return date in the format day month year (ex 14 Apr 2016) */ fun getDateAsString(integersOfDate: List): String { + logger.d { "ktorClient $integersOfDate" } val stringBuilder = StringBuilder() stringBuilder.append(integersOfDate[2]) .append(' ') @@ -68,6 +70,27 @@ object DateHelper { ) } + private val monthMap = mapOf( + "Jan" to 1, "Feb" to 2, "Mar" to 3, "Apr" to 4, + "May" to 5, "Jun" to 6, "Jul" to 7, "Aug" to 8, + "Sep" to 9, "Oct" to 10, "Nov" to 11, "Dec" to 12, + ) + + fun getMonthNumber(monthName: String): Int { + return monthMap[monthName] + ?: throw IllegalArgumentException("Invalid month name: $monthName") + } + + fun getDateAsList(date: String): List { + val dateList = date.split(" ") + + val day = dateList[0].toInt() + val month = getMonthNumber(dateList[1]) + val year = dateList[2].toInt() + + return listOf(year, month, day) + } + /** * This Method converting the dd-MM-yyyy format type date string into dd MMMM yyyy * diff --git a/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/FormatAmount.kt b/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/FormatAmount.kt new file mode 100644 index 000000000..468f85b08 --- /dev/null +++ b/core/common/src/commonMain/kotlin/org/mifos/mobile/core/common/FormatAmount.kt @@ -0,0 +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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +expect fun formatAmount(amount: Double): String diff --git a/core/common/src/desktopMain/kotlin/org/mifos/mobile/core/common/FormatAmount.desktop.kt b/core/common/src/desktopMain/kotlin/org/mifos/mobile/core/common/FormatAmount.desktop.kt new file mode 100644 index 000000000..a58152676 --- /dev/null +++ b/core/common/src/desktopMain/kotlin/org/mifos/mobile/core/common/FormatAmount.desktop.kt @@ -0,0 +1,17 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +import java.text.DecimalFormat + +actual fun formatAmount(amount: Double): String { + val formatter = DecimalFormat("#,##0.00") // Ensures two decimal places + return formatter.format(amount) +} diff --git a/core/common/src/jsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.js.kt b/core/common/src/jsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.js.kt new file mode 100644 index 000000000..88914ede0 --- /dev/null +++ b/core/common/src/jsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.js.kt @@ -0,0 +1,14 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +actual fun formatAmount(amount: Double): String { + return amount.asDynamic().toFixed(2) as String +} diff --git a/core/common/src/nativeMain/kotlin/org/mifos/mobile/core/common/FormatAmount.native.kt b/core/common/src/nativeMain/kotlin/org/mifos/mobile/core/common/FormatAmount.native.kt new file mode 100644 index 000000000..c139d03fa --- /dev/null +++ b/core/common/src/nativeMain/kotlin/org/mifos/mobile/core/common/FormatAmount.native.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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterDecimalStyle + +actual fun formatAmount(amount: Double): String { + val formatter = NSNumberFormatter().apply { + numberStyle = NSNumberFormatterDecimalStyle + maximumFractionDigits = 2.toULong() + minimumFractionDigits = 2.toULong() + } + return formatter.stringFromNumber(NSNumber(amount)) ?: amount.toString() +} diff --git a/core/common/src/wasmJsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.wasmJs.kt b/core/common/src/wasmJsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.wasmJs.kt new file mode 100644 index 000000000..ab22f2c45 --- /dev/null +++ b/core/common/src/wasmJsMain/kotlin/org/mifos/mobile/core/common/FormatAmount.wasmJs.kt @@ -0,0 +1,17 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.common + +@JsFun("a => a.toFixed(2)") +external fun jsToFixed(amount: Double): String + +actual fun formatAmount(amount: Double): String { + return jsToFixed(amount) +} diff --git a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/LoanRepositoryImp.kt b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/LoanRepositoryImp.kt index 716716a9b..dbada46fc 100644 --- a/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/LoanRepositoryImp.kt +++ b/core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/LoanRepositoryImp.kt @@ -11,9 +11,12 @@ package org.mifos.mobile.core.data.repositoryImpl import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.common.FileUtils.Companion.logger import org.mifos.mobile.core.common.asDataStateFlow import org.mifos.mobile.core.data.repository.LoanRepository import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations @@ -31,7 +34,14 @@ class LoanRepositoryImp( loanId: Long?, ): Flow> { return dataManager.loanAccountsListApi.getLoanWithAssociations(loanId!!, associationType) - .asDataStateFlow().flowOn(ioDispatcher) + .map { response -> + logger.d { "success Getting loan details from server repo $response" } + DataState.Success(response) + }.catch { exception -> + logger.e { "Error fetching loan details: ${exception.message}" } + DataState.Error(exception, null) + } + .flowOn(ioDispatcher) } override suspend fun withdrawLoanAccount( diff --git a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithAssociations.kt b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithAssociations.kt index 4c02a1fbf..f4e746eda 100644 --- a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithAssociations.kt +++ b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithAssociations.kt @@ -61,7 +61,7 @@ data class LoanWithAssociations( val repaymentFrequencyType: RepaymentFrequencyType? = null, - val interestRatePerPeriod: Int? = null, + val interestRatePerPeriod: Double? = null, val interestRateFrequencyType: InterestRateFrequencyType? = null, diff --git a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithdraw.kt b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithdraw.kt index 3cfdbe3dc..c89649e91 100644 --- a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithdraw.kt +++ b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/LoanWithdraw.kt @@ -9,9 +9,11 @@ */ package org.mifos.mobile.core.model.entity.accounts.loan +import kotlinx.serialization.Serializable import org.mifos.mobile.core.model.Parcelable import org.mifos.mobile.core.model.Parcelize +@Serializable @Parcelize data class LoanWithdraw( val withdrawnOnDate: String? = null, diff --git a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/calendardata/CalendarData.kt b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/calendardata/CalendarData.kt index 0f217bdc2..21fec810a 100644 --- a/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/calendardata/CalendarData.kt +++ b/core/model/src/commonMain/kotlin/org/mifos/mobile/core/model/entity/accounts/loan/calendardata/CalendarData.kt @@ -50,9 +50,9 @@ data class CalendarData( val humanReadable: String? = null, - val createdDate: List = emptyList(), + val createdDate: String? = null, - val lastUpdatedDate: List = emptyList(), + val lastUpdatedDate: String? = null, val createdByUserId: Int? = null, diff --git a/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/viewmodel/LoanAccountViewmodel.kt b/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/viewmodel/LoanAccountViewmodel.kt index d65c4689b..84d53089e 100644 --- a/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/viewmodel/LoanAccountViewmodel.kt +++ b/feature/loan-account/src/commonMain/kotlin/org/mifos/mobile/feature/loanaccount/viewmodel/LoanAccountViewmodel.kt @@ -7,6 +7,8 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ +@file:Suppress("ktlint:standard:property-naming") + package org.mifos.mobile.feature.loanaccount.viewmodel import androidx.lifecycle.ViewModel @@ -19,11 +21,11 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource +import org.mifos.mobile.core.common.Constants import org.mifos.mobile.core.data.repository.AccountsRepository import org.mifos.mobile.core.data.util.NetworkMonitor import org.mifos.mobile.core.datastore.UserPreferencesRepository import org.mifos.mobile.core.model.entity.accounts.loan.LoanAccount -import org.mifos.mobile.core.model.enums.AccountType import org.mifos.mobile.feature.loanaccount.utils.AccountState import org.mifos.mobile.feature.loanaccount.utils.FilterUtil @@ -63,7 +65,6 @@ class LoanAccountViewmodel( ) /** Holds the current state of loan accounts UI. */ - @Suppress("PropertyName") private val _accountsUiState = MutableStateFlow(AccountState.Loading) val accountUiState: StateFlow = _accountsUiState.asStateFlow() @@ -182,7 +183,7 @@ class LoanAccountViewmodel( _accountsUiState.value = AccountState.Loading accountsRepositoryImpl.loadAccounts( clientId = clientId, - accountType = AccountType.LOAN.name, + accountType = Constants.LOAN_ACCOUNTS, ).catch { _accountsUiState.value = AccountState.Error }.collect { clientAccounts -> diff --git a/feature/loan/build.gradle.kts b/feature/loan/build.gradle.kts index 9a4d254d5..5abbefe0b 100644 --- a/feature/loan/build.gradle.kts +++ b/feature/loan/build.gradle.kts @@ -8,14 +8,28 @@ * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifos.android.feature) - alias(libs.plugins.mifos.android.library.compose) + alias(libs.plugins.mifos.cmp.feature) + alias(libs.plugins.kotlin.serialization) + id(libs.plugins.kotlin.parcelize.get().pluginId) } + android { namespace = "org.mifos.mobile.feature.loan" } -dependencies { - implementation(projects.core.qrcode) +kotlin { + sourceSets { + commonMain.dependencies { + api(libs.kermit.logging) + implementation(compose.material3) + implementation(compose.foundation) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.jb.kotlin.stdlib) + implementation(libs.kotlin.reflect) + implementation(libs.kotlinx.serialization.json) + } + } } \ No newline at end of file diff --git a/feature/loan/src/main/res/drawable/ic_assignment_turned_in_black_24dp.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_assignment_turned_in_black_24dp.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_assignment_turned_in_black_24dp.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_assignment_turned_in_black_24dp.xml diff --git a/feature/loan/src/main/res/drawable/ic_charges.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_charges.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_charges.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_charges.xml diff --git a/feature/loan/src/main/res/drawable/ic_check_circle_green_24px.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_check_circle_green_24px.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_check_circle_green_24px.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_check_circle_green_24px.xml diff --git a/feature/loan/src/main/res/drawable/ic_compare_arrows_black_24dp.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_compare_arrows_black_24dp.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_compare_arrows_black_24dp.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_compare_arrows_black_24dp.xml diff --git a/feature/loan/src/main/res/drawable/ic_edit_black_24dp.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_edit_black_24dp.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_edit_black_24dp.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_edit_black_24dp.xml diff --git a/feature/loan/src/main/res/drawable/ic_error_black_24dp.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_error_black_24dp.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_error_black_24dp.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_error_black_24dp.xml diff --git a/feature/loan/src/main/res/drawable/ic_local_atm_black_24dp.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_local_atm_black_24dp.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_local_atm_black_24dp.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_local_atm_black_24dp.xml diff --git a/feature/loan/src/main/res/drawable/ic_qrcode_scan.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_qrcode_scan.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_qrcode_scan.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_qrcode_scan.xml diff --git a/feature/loan/src/main/res/drawable/ic_report_problem_red_24px.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_report_problem_red_24px.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_report_problem_red_24px.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_report_problem_red_24px.xml diff --git a/feature/loan/src/main/res/drawable/ic_surveys_48px.xml b/feature/loan/src/commonMain/composeResources/drawable/ic_surveys_48px.xml similarity index 100% rename from feature/loan/src/main/res/drawable/ic_surveys_48px.xml rename to feature/loan/src/commonMain/composeResources/drawable/ic_surveys_48px.xml diff --git a/feature/loan/src/main/res/values/strings.xml b/feature/loan/src/commonMain/composeResources/values/strings.xml similarity index 100% rename from feature/loan/src/main/res/values/strings.xml rename to feature/loan/src/commonMain/composeResources/values/strings.xml diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/di/LoanModule.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/di/LoanModule.kt new file mode 100644 index 000000000..626183130 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/di/LoanModule.kt @@ -0,0 +1,31 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifos.mobile.feature.loan.loanAccount.LoanAccountsDetailViewModel +import org.mifos.mobile.feature.loan.loanAccountApplication.LoanApplicationViewModel +import org.mifos.mobile.feature.loan.loanAccountSummary.LoanAccountSummaryViewModel +import org.mifos.mobile.feature.loan.loanAccountTransaction.LoanAccountTransactionViewModel +import org.mifos.mobile.feature.loan.loanAccountWithdraw.LoanAccountWithdrawViewModel +import org.mifos.mobile.feature.loan.loanRepaymentSchedule.LoanRepaymentScheduleViewModel +import org.mifos.mobile.feature.loan.loanReview.ReviewLoanApplicationViewModel + +val LoanModule = module { + + viewModelOf(::LoanAccountsDetailViewModel) + viewModelOf(::LoanApplicationViewModel) + viewModelOf(::LoanAccountSummaryViewModel) + viewModelOf(::LoanAccountTransactionViewModel) + viewModelOf(::LoanAccountWithdrawViewModel) + viewModelOf(::LoanRepaymentScheduleViewModel) + viewModelOf(::ReviewLoanApplicationViewModel) +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt similarity index 58% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt index d848ecdb1..53365e117 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailContent.kt @@ -9,6 +9,7 @@ */ package org.mifos.mobile.feature.loan.loanAccount +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -17,23 +18,46 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import org.mifos.mobile.core.common.utils.CurrencyUtil -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.designsystem.components.MifosButton +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_number +import mifos_mobile.feature.loan.generated.resources.currency +import mifos_mobile.feature.loan.generated.resources.due_date +import mifos_mobile.feature.loan.generated.resources.ic_charges +import mifos_mobile.feature.loan.generated.resources.ic_compare_arrows_black_24dp +import mifos_mobile.feature.loan.generated.resources.ic_qrcode_scan +import mifos_mobile.feature.loan.generated.resources.ic_surveys_48px +import mifos_mobile.feature.loan.generated.resources.loan_charges +import mifos_mobile.feature.loan.generated.resources.loan_summary +import mifos_mobile.feature.loan.generated.resources.loan_type +import mifos_mobile.feature.loan.generated.resources.make_payment +import mifos_mobile.feature.loan.generated.resources.monitor +import mifos_mobile.feature.loan.generated.resources.next_installment +import mifos_mobile.feature.loan.generated.resources.not_available +import mifos_mobile.feature.loan.generated.resources.outstanding_balance +import mifos_mobile.feature.loan.generated.resources.qr_code +import mifos_mobile.feature.loan.generated.resources.repayment_schedule +import mifos_mobile.feature.loan.generated.resources.string_and_string +import mifos_mobile.feature.loan.generated.resources.transactions +import mifos_mobile.feature.loan.generated.resources.view_charges +import mifos_mobile.feature.loan.generated.resources.view_loan_summary +import mifos_mobile.feature.loan.generated.resources.view_qr_code +import mifos_mobile.feature.loan.generated.resources.view_repayment +import mifos_mobile.feature.loan.generated.resources.view_transactions +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.common.CurrencyFormatter +import org.mifos.mobile.core.common.DateHelper +import org.mifos.mobile.core.designsystem.component.MifosButton +import org.mifos.mobile.core.designsystem.component.MifosCard import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations import org.mifos.mobile.core.ui.component.MifosTextTitleDescDoubleLine import org.mifos.mobile.core.ui.component.MonitorListItemWithIcon -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R @Composable internal fun LoanAccountDetailContent( @@ -47,20 +71,18 @@ internal fun LoanAccountDetailContent( modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() - Column( modifier = modifier .fillMaxWidth() .verticalScroll(scrollState) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { LoanAccountDetailsCard( loanWithAssociations = loanWithAssociations, makePayment = makePayment, ) - Spacer(modifier = Modifier.height(16.dp)) - LoanMonitorComponent( viewLoanSummary = viewLoanSummary, viewCharges = viewCharges, @@ -77,76 +99,70 @@ private fun LoanAccountDetailsCard( makePayment: () -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val isActive = loanWithAssociations.status?.active == true val currencySymbol = loanWithAssociations.summary?.currency?.displaySymbol ?: "$" val dueDate = if (isActive) { val overdueSinceDate = loanWithAssociations.summary?.getOverdueSinceDate() overdueSinceDate?.let { DateHelper.getDateAsString(it) } - ?: stringResource(R.string.not_available) + ?: stringResource(Res.string.not_available) } else { - stringResource(R.string.not_available) + stringResource(Res.string.not_available) } val nextInstallment = getNextInstallment(loanWithAssociations, currencySymbol) - OutlinedCard(modifier = modifier) { - Column(modifier = Modifier.padding(14.dp)) { + MifosCard(modifier = modifier) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.outstanding_balance), + title = stringResource(Res.string.outstanding_balance), description = stringResource( - R.string.string_and_string, + Res.string.string_and_string, currencySymbol, - CurrencyUtil.formatCurrency( - context = context, - amt = loanWithAssociations.summary?.totalOutstanding, + CurrencyFormatter.format( + loanWithAssociations.summary?.totalOutstanding, + currencySymbol, + 5, ), ), descriptionStyle = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.next_installment), + title = stringResource(Res.string.next_installment), description = nextInstallment, descriptionStyle = MaterialTheme.typography.bodyLarge, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.due_date), + title = stringResource(Res.string.due_date), description = dueDate, descriptionStyle = MaterialTheme.typography.bodyLarge, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.account_number), + title = stringResource(Res.string.account_number), description = loanWithAssociations.accountNo ?: "", descriptionStyle = MaterialTheme.typography.bodyLarge, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.loan_type), + title = stringResource(Res.string.loan_type), description = loanWithAssociations.loanType?.value ?: "", descriptionStyle = MaterialTheme.typography.bodyLarge, ) - Spacer(modifier = Modifier.height(8.dp)) MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.currency), + title = stringResource(Res.string.currency), description = loanWithAssociations.summary?.currency?.code ?: "", descriptionStyle = MaterialTheme.typography.bodyLarge, ) MifosButton( - textResId = R.string.make_payment, + text = { stringResource(Res.string.make_payment) }, modifier = Modifier.fillMaxWidth(), enabled = isActive, onClick = makePayment, @@ -164,9 +180,9 @@ private fun LoanMonitorComponent( viewQr: () -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxWidth()) { + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) { Text( - text = stringResource(id = R.string.monitor), + text = stringResource(Res.string.monitor), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth(), @@ -175,37 +191,37 @@ private fun LoanMonitorComponent( Spacer(modifier = Modifier.height(8.dp)) MonitorListItemWithIcon( - titleId = R.string.loan_summary, - subTitleId = R.string.view_loan_summary, - iconId = R.drawable.ic_surveys_48px, + titleId = Res.string.loan_summary, + subTitleId = Res.string.view_loan_summary, + iconId = Res.drawable.ic_surveys_48px, onClick = { viewLoanSummary.invoke() }, ) MonitorListItemWithIcon( - titleId = R.string.loan_charges, - subTitleId = R.string.view_charges, - iconId = R.drawable.ic_charges, + titleId = Res.string.loan_charges, + subTitleId = Res.string.view_charges, + iconId = Res.drawable.ic_charges, onClick = { viewCharges.invoke() }, ) MonitorListItemWithIcon( - titleId = R.string.repayment_schedule, - subTitleId = R.string.view_repayment, - iconId = R.drawable.ic_charges, + titleId = Res.string.repayment_schedule, + subTitleId = Res.string.view_repayment, + iconId = Res.drawable.ic_charges, onClick = { viewRepaymentSchedule.invoke() }, ) MonitorListItemWithIcon( - titleId = R.string.transactions, - subTitleId = R.string.view_transactions, - iconId = R.drawable.ic_compare_arrows_black_24dp, + titleId = Res.string.transactions, + subTitleId = Res.string.view_transactions, + iconId = Res.drawable.ic_compare_arrows_black_24dp, onClick = { viewTransactions.invoke() }, ) MonitorListItemWithIcon( - titleId = R.string.qr_code, - subTitleId = R.string.view_qr_code, - iconId = R.drawable.ic_qrcode_scan, + titleId = Res.string.qr_code, + subTitleId = Res.string.view_qr_code, + iconId = Res.drawable.ic_qrcode_scan, onClick = { viewQr.invoke() }, ) } @@ -220,19 +236,20 @@ private fun getNextInstallment( val dueDate = period.dueDate if (dueDate == loanWithAssociations.summary?.getOverdueSinceDate()) { return stringResource( - id = R.string.string_and_string, + Res.string.string_and_string, currencySymbol, - CurrencyUtil.formatCurrency( - context = LocalContext.current, - amt = period.totalDueForPeriod ?: 0.0, + CurrencyFormatter.format( + period.totalDueForPeriod ?: 0.0, + currencySymbol, + 5, ), ) } } - return stringResource(id = R.string.not_available) + return stringResource(Res.string.not_available) } -@DevicePreviews +@Preview @Composable private fun LoanAccountDetailContentPreview() { MifosMobileTheme { diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt new file mode 100644 index 000000000..1edcffc4c --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccount + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.approval_pending +import mifos_mobile.feature.loan.generated.resources.loan_account_details +import mifos_mobile.feature.loan.generated.resources.no_internet_connection +import mifos_mobile.feature.loan.generated.resources.waiting_for_disburse +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.common.Constants.TRANSFER_PAY_TO +import org.mifos.mobile.core.designsystem.component.MifosScaffold +import org.mifos.mobile.core.designsystem.icon.MifosIcons +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme +import org.mifos.mobile.core.ui.component.EmptyDataView +import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.component.NoInternet +import org.mifos.mobile.core.ui.utils.EventsEffect + +@Composable +internal fun LoanAccountDetailScreen( + navigateBack: () -> Unit, + viewGuarantor: (loanId: Long) -> Unit, + updateLoan: (Long) -> Unit, + withdrawLoan: (Long) -> Unit, + viewLoanSummary: (Long) -> Unit, + viewCharges: () -> Unit, + viewRepaymentSchedule: (Long) -> Unit, + viewTransactions: (Long) -> Unit, + viewQr: (String) -> Unit, + makePayment: (accountId: Long, outstandingBalance: Double?, transferType: String) -> Unit, + modifier: Modifier = Modifier, + viewModel: LoanAccountsDetailViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val outStanding = state.loanAccountAssociations?.summary?.totalOutstanding + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + is LoanAccountsEvent.NavigateBack -> navigateBack.invoke() + is LoanAccountsEvent.ViewGuarantor -> state.loanId?.let { viewGuarantor(it) } + is LoanAccountsEvent.MakePayment -> state.loanId?.let { makePayment(it, outStanding, TRANSFER_PAY_TO) } + is LoanAccountsEvent.UpdateLoan -> state.loanId?.let { updateLoan(it) } + is LoanAccountsEvent.ViewCharges -> viewCharges() + is LoanAccountsEvent.ViewLoanSummary -> state.loanId?.let { viewLoanSummary(it) } + is LoanAccountsEvent.ViewQr -> viewQr(state.loanId.toString()) + is LoanAccountsEvent.ViewRepaymentSchedule -> state.loanId?.let { + viewRepaymentSchedule(it) + } + is LoanAccountsEvent.ViewTransactions -> state.loanId?.let { viewTransactions(it) } + is LoanAccountsEvent.WithDrawLoan -> state.loanId?.let { withdrawLoan(it) } + is LoanAccountsEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + LoanAccountDetailDialog( + dialogState = state.dialogState, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + state = state, + ) + + LoanAccountDetailScreen( + state = state, + modifier = modifier, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) +} + +@Composable +private fun LoanAccountDetailDialog( + dialogState: LoanAccountsState.DialogState?, + onAction: (LoanAccountAction) -> Unit, + state: LoanAccountsState, +) { + when (dialogState) { + is LoanAccountsState.DialogState.Error -> { + ErrorComponent( + retryConnection = { + onAction( + LoanAccountAction + .RetryConnectionClicked, + ) + }, + isOnline = state.isOnline, + ) + } + + is LoanAccountsState.DialogState.Loading -> MifosProgressIndicator(modifier = Modifier.fillMaxSize()) + LoanAccountsState.DialogState.ApprovalPending -> EmptyDataView( + modifier = Modifier.fillMaxSize(), + icon = MifosIcons.Error, + error = Res.string.approval_pending, + ) + LoanAccountsState.DialogState.WaitingForDisburse -> EmptyDataView( + modifier = Modifier.fillMaxSize(), + icon = MifosIcons.Error, + error = Res.string.waiting_for_disburse, + ) + null -> Unit + } +} + +@Composable +private fun LoanAccountDetailScreen( + state: LoanAccountsState, + onAction: (LoanAccountAction) -> Unit, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + topBar = { + LoanAccountDetailTopBar( + navigateBack = { onAction(LoanAccountAction.BackPress) }, + viewGuarantor = { onAction(LoanAccountAction.ViewGuarantorClicked) }, + updateLoan = { onAction(LoanAccountAction.UpdateLoanClicked) }, + withdrawLoan = { onAction(LoanAccountAction.WithDrawLoanClicked) }, + ) + }, + content = { + Box(modifier = Modifier.padding(it)) { + state.loanAccountAssociations?.let { loan -> + LoanAccountDetailContent( + loanWithAssociations = loan, + viewLoanSummary = { onAction(LoanAccountAction.ViewLoanSummaryClicked) }, + viewCharges = { onAction(LoanAccountAction.ViewCharges) }, + viewRepaymentSchedule = { onAction(LoanAccountAction.ViewRepaymentScheduleClicked) }, + viewTransactions = { onAction(LoanAccountAction.ViewTransactionsClicked) }, + viewQr = { onAction(LoanAccountAction.ViewQRClicked) }, + makePayment = { onAction(LoanAccountAction.MakePaymentClicked) }, + ) + } + } + }, + ) +} + +@Composable +private fun ErrorComponent( + retryConnection: () -> Unit, + isOnline: Boolean, +) { + if (!isOnline) { + NoInternet( + error = Res.string.no_internet_connection, + isRetryEnabled = true, + retry = retryConnection, + ) + } else { + EmptyDataView( + icon = MifosIcons.Error, + error = Res.string.loan_account_details, + modifier = Modifier.fillMaxSize(), + ) + } +} + +@Composable +@Preview +private fun LoanAccountDetailScreenPreview() { + MifosMobileTheme { + LoanAccountDetailScreen( + state = LoanAccountsState(dialogState = null), + modifier = Modifier, + onAction = {}, + ) + } +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt similarity index 78% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt index 6a68e09fb..f548a86bb 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailTopBar.kt @@ -25,12 +25,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.mifos.mobile.core.designsystem.icons.MifosIcons +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.loan_account_details +import mifos_mobile.feature.loan.generated.resources.update_loan +import mifos_mobile.feature.loan.generated.resources.view_guarantor +import mifos_mobile.feature.loan.generated.resources.withdraw_loan +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.icon.MifosIcons import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,15 +49,14 @@ internal fun LoanAccountDetailTopBar( TopAppBar( modifier = modifier, - title = { Text(text = stringResource(id = R.string.loan_account_details)) }, + title = { Text(text = stringResource(Res.string.loan_account_details)) }, navigationIcon = { IconButton( - onClick = { navigateBack.invoke() }, + onClick = navigateBack, ) { Icon( imageVector = MifosIcons.ArrowBack, contentDescription = "Back Arrow", - tint = MaterialTheme.colorScheme.onSurface, ) } }, @@ -65,7 +68,6 @@ internal fun LoanAccountDetailTopBar( Icon( imageVector = MifosIcons.MoreVert, contentDescription = "Menu", - tint = MaterialTheme.colorScheme.onSurface, ) } @@ -76,19 +78,19 @@ internal fun LoanAccountDetailTopBar( ) { DropdownMenuItem( text = { - Text(text = stringResource(id = R.string.view_guarantor)) + Text(text = stringResource(Res.string.view_guarantor)) }, onClick = viewGuarantor, ) DropdownMenuItem( text = { - Text(text = stringResource(id = R.string.update_loan)) + Text(text = stringResource(Res.string.update_loan)) }, onClick = updateLoan, ) DropdownMenuItem( text = { - Text(text = stringResource(id = R.string.withdraw_loan)) + Text(text = stringResource(Res.string.withdraw_loan)) }, onClick = withdrawLoan, ) @@ -97,7 +99,7 @@ internal fun LoanAccountDetailTopBar( ) } -@DevicePreviews +@Preview @Composable private fun LoanAccountDetailTopBarPreview() { MifosMobileTheme { diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt new file mode 100644 index 000000000..efe650bbf --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccount + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.internet_not_connected +import org.jetbrains.compose.resources.getString +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.common.Constants.LOAN_ID +import org.mifos.mobile.core.common.Constants.TRANSFER_PAY_TO +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanAccountsDetailViewModel( + private val loanRepositoryImp: LoanRepository, + // TODO() this repository injection is for generating QR code by taking user details from datastore +// private val userPreferencesRepositoryImpl: UserPreferencesRepository, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = LoanAccountsState( + dialogState = null, + loanId = savedStateHandle.getStateFlow(key = LOAN_ID, initialValue = null).value, + ), +) { + + init { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + updateState { it.copy(isOnline = isConnected) } + if (!isConnected) { + sendEvent(LoanAccountsEvent.ShowToast(getString(Res.string.internet_not_connected))) + } + } + } + loadLoanAccountDetails() + } + + private fun updateState(update: (LoanAccountsState) -> LoanAccountsState) { + mutableStateFlow.update(update) + } + + override fun handleAction(action: LoanAccountAction) { + when (action) { + is LoanAccountAction.LoanIdChanged -> { + updateState { it.copy(loanId = state.loanId) } + } + + LoanAccountAction.BackPress -> { + sendEvent(LoanAccountsEvent.NavigateBack) + } + + LoanAccountAction.MakePaymentClicked -> { + sendEvent( + LoanAccountsEvent.MakePayment( + state.loanId, + state.loanAccountAssociations?.summary?.totalOutstanding ?: 1.00, + TRANSFER_PAY_TO, + ), + ) + } + + LoanAccountAction.UpdateLoanClicked -> { + sendEvent(LoanAccountsEvent.UpdateLoan(state.loanId)) + } + + LoanAccountAction.ViewCharges -> { + sendEvent(LoanAccountsEvent.ViewCharges) + } + + LoanAccountAction.ViewGuarantorClicked -> { + sendEvent(LoanAccountsEvent.ViewGuarantor(state.loanId)) + } + + LoanAccountAction.ViewLoanSummaryClicked -> { + sendEvent(LoanAccountsEvent.ViewLoanSummary(state.loanId)) + } + + LoanAccountAction.ViewQRClicked -> { + sendEvent(LoanAccountsEvent.ViewQr(state.loanId)) + } + + LoanAccountAction.ViewRepaymentScheduleClicked -> { + sendEvent(LoanAccountsEvent.ViewRepaymentSchedule(state.loanId)) + } + + LoanAccountAction.ViewTransactionsClicked -> { + sendEvent(LoanAccountsEvent.ViewTransactions(state.loanId)) + } + + LoanAccountAction.WithDrawLoanClicked -> { + sendEvent(LoanAccountsEvent.WithDrawLoan(state.loanId)) + } + + LoanAccountAction.RetryConnectionClicked -> { + loadLoanAccountDetails() + } + } + } + + private fun loadLoanAccountDetails() { + viewModelScope.launch { + updateState { + it.copy(dialogState = LoanAccountsState.DialogState.Loading) + } + loanRepositoryImp.getLoanWithAssociations( + loanId = state.loanId, + associationType = Constants.REPAYMENT_SCHEDULE, +// associationType = null + ).catch { + exception -> + updateState { + it.copy( + dialogState = LoanAccountsState.DialogState.Error( + exception.message ?: "An error occurred", + ), + ) + } + } + .collect { result -> + updateState { currentState -> + when (result) { + is DataState.Error -> { + currentState.copy( + dialogState = LoanAccountsState.DialogState.Error( + result.exception.message ?: "An error occurred", + ), + ) + } + + is DataState.Loading -> { + currentState.copy(dialogState = LoanAccountsState.DialogState.Loading) + } + + is DataState.Success -> { + val loan = result.data + when { + loan == null -> currentState.copy( + dialogState = LoanAccountsState.DialogState.Error("Accounts not found"), + ) + + loan.status?.active == true -> { + currentState.copy( + loanAccountAssociations = loan, + dialogState = null, + ) + } + + loan.status?.pendingApproval == true -> currentState.copy( + dialogState = LoanAccountsState.DialogState.ApprovalPending, + ) + + loan.status?.waitingForDisbursal == true -> currentState.copy( + dialogState = LoanAccountsState.DialogState.WaitingForDisburse, + ) + + else -> currentState.copy( + loanAccountAssociations = loan, + dialogState = null, + ) + } + } + } + } + } + } + } + + // TODO After migrating QR code module to CMP, implement this function and use + +// fun getQrString(): String { +// return QrCodeGenerator.getAccountDetailsInString( +// loanWithAssociations?.accountNo, +// preferencesHelper.officeName, +// AccountType.LOAN, +// ) +// } +} + +@Parcelize +data class LoanAccountsState( + val loanId: Long? = -1L, + val dialogState: DialogState?, + val loanAccountAssociations: LoanWithAssociations? = null, + val isOnline: Boolean = false, +) : Parcelable { + + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + + @Parcelize + data object ApprovalPending : DialogState + + @Parcelize + data object WaitingForDisburse : DialogState + } +} + +sealed interface LoanAccountsEvent { + data object NavigateBack : LoanAccountsEvent + data class ViewGuarantor(val loanId: Long?) : LoanAccountsEvent + data class UpdateLoan(val loanId: Long?) : LoanAccountsEvent + data class WithDrawLoan(val loanId: Long?) : LoanAccountsEvent + data class ViewLoanSummary(val loanId: Long?) : LoanAccountsEvent + data object ViewCharges : LoanAccountsEvent + data class ViewRepaymentSchedule(val loanId: Long?) : LoanAccountsEvent + data class ViewTransactions(val loanId: Long?) : LoanAccountsEvent + data class ViewQr(val loanId: Long?) : LoanAccountsEvent + data class MakePayment(val loanId: Long?, val outStanding: Double, val transferTo: String) : + LoanAccountsEvent + data class ShowToast(val message: String) : LoanAccountsEvent +} + +sealed interface LoanAccountAction { + data class LoanIdChanged(val loanId: Long) : LoanAccountAction + data object BackPress : LoanAccountAction + data object ViewGuarantorClicked : LoanAccountAction + data object UpdateLoanClicked : LoanAccountAction + data object WithDrawLoanClicked : LoanAccountAction + data object ViewLoanSummaryClicked : LoanAccountAction + data object ViewCharges : LoanAccountAction + data object ViewRepaymentScheduleClicked : LoanAccountAction + data object ViewTransactionsClicked : LoanAccountAction + data object ViewQRClicked : LoanAccountAction + data object MakePaymentClicked : LoanAccountAction + data object RetryConnectionClicked : LoanAccountAction +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt similarity index 50% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt index d785c32ff..3be3fe399 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationContent.kt @@ -10,15 +10,14 @@ package org.mifos.mobile.feature.loan.loanAccountApplication import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.DatePicker -import androidx.compose.material3.DatePickerDefaults import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -31,55 +30,84 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.common.utils.DateHelper.FORMAT_MM -import org.mifos.mobile.core.designsystem.components.MifosButton -import org.mifos.mobile.core.designsystem.components.MifosOutlinedTextField -import org.mifos.mobile.core.designsystem.components.MifosTextButton +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_number +import mifos_mobile.feature.loan.generated.resources.amount_greater_than_zero +import mifos_mobile.feature.loan.generated.resources.currency +import mifos_mobile.feature.loan.generated.resources.dialog_action_cancel +import mifos_mobile.feature.loan.generated.resources.dialog_action_ok +import mifos_mobile.feature.loan.generated.resources.enter_amount +import mifos_mobile.feature.loan.generated.resources.expected_disbursement_date +import mifos_mobile.feature.loan.generated.resources.ic_edit_black_24dp +import mifos_mobile.feature.loan.generated.resources.loan_name +import mifos_mobile.feature.loan.generated.resources.new_loan_application +import mifos_mobile.feature.loan.generated.resources.principal_amount +import mifos_mobile.feature.loan.generated.resources.purpose_of_loan +import mifos_mobile.feature.loan.generated.resources.review +import mifos_mobile.feature.loan.generated.resources.select_loan_product +import mifos_mobile.feature.loan.generated.resources.select_loan_product_field +import mifos_mobile.feature.loan.generated.resources.string_and_string +import mifos_mobile.feature.loan.generated.resources.submission_date +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.common.DateHelper +import org.mifos.mobile.core.common.DateHelper.format +import org.mifos.mobile.core.designsystem.component.MifosButton +import org.mifos.mobile.core.designsystem.component.MifosOutlinedTextField +import org.mifos.mobile.core.designsystem.component.MifosTextButton +import org.mifos.mobile.core.designsystem.component.MifosTextFieldConfig import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.ui.component.MifosDropDownTextField import org.mifos.mobile.core.ui.component.MifosTextTitleDescDrawableSingleLine import org.mifos.mobile.core.ui.component.MifosTextTitleDescSingleLine -import org.mifos.mobile.core.ui.utils.DevicePreviews import org.mifos.mobile.core.ui.utils.PresentOrFutureSelectableDates -import org.mifos.mobile.feature.loan.R +@Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun LoanApplicationContent( - uiData: LoanApplicationScreenData, + state: LoanApplicationState, selectProduct: (Int) -> Unit, selectPurpose: (Int) -> Unit, setDisbursementDate: (String) -> Unit, reviewClicked: (String) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val scrollState = rememberScrollState() var purposeTextFieldEnable by rememberSaveable { mutableStateOf(false) } var selectedLoanProductError by rememberSaveable { mutableStateOf(null) } var showSelectedLoanProductError by rememberSaveable { mutableStateOf(false) } - var expectedDisbursementDate by rememberSaveable { mutableStateOf(uiData.disbursementDate) } + var expectedDisbursementDate by rememberSaveable { + mutableStateOf( + state.loanWithAssociations?.timeline?.expectedDisbursementDate, + ) + } var showDatePicker by rememberSaveable { mutableStateOf(false) } - var selectedLoanProduct by rememberSaveable { mutableStateOf(uiData.selectedLoanProduct) } - var selectedLoanPurpose by rememberSaveable { mutableStateOf(uiData.selectedLoanPurpose) } + var selectedLoanProduct by rememberSaveable { mutableStateOf(state.selectedLoanProduct) } + var selectedLoanPurpose by rememberSaveable { mutableStateOf(state.selectedLoanPurpose) } val datePickerState = rememberDatePickerState(selectableDates = PresentOrFutureSelectableDates) var principalAmount by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(uiData.principalAmount ?: "")) + mutableStateOf(TextFieldValue(state.loanWithAssociations?.principal.toString())) } + var principalAmountError by rememberSaveable { mutableStateOf(null) } var showPrincipalAmountError by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(key1 = uiData) { - principalAmount = TextFieldValue(uiData.principalAmount ?: "") + LaunchedEffect(key1 = state) { + state.loanWithAssociations?.timeline?.expectedDisbursementDate?.let { + expectedDisbursementDate = it + } + principalAmount = TextFieldValue(state.principalAmount ?: "") } LaunchedEffect(key1 = selectedLoanProduct) { @@ -87,7 +115,7 @@ internal fun LoanApplicationContent( showSelectedLoanProductError = false showPrincipalAmountError = false selectedLoanProductError = when { - uiData.selectedLoanProduct.isNullOrBlank() -> context.getString(R.string.select_loan_product_field) + state.selectedLoanProduct.isNullOrBlank() -> getString(Res.string.select_loan_product_field) else -> null } } @@ -95,54 +123,49 @@ internal fun LoanApplicationContent( LaunchedEffect(key1 = principalAmount) { showPrincipalAmountError = false principalAmountError = when { - principalAmount.text.isBlank() -> context.getString(R.string.enter_amount) - principalAmount.text.matches("^0*".toRegex()) -> context.getString(R.string.amount_greater_than_zero) + principalAmount.text.isBlank() -> getString(Res.string.enter_amount) + principalAmount.text.matches("^0*".toRegex()) -> getString(Res.string.amount_greater_than_zero) else -> null } } Column( - modifier = modifier + modifier = Modifier .verticalScroll(scrollState) .background(color = MaterialTheme.colorScheme.background) .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( style = MaterialTheme.typography.bodyMedium, - text = if (uiData.clientName != null) { + text = if (state.loanWithAssociations?.clientName != null) { stringResource( - id = R.string.string_and_string, - stringResource(id = R.string.new_loan_application) + " ", - uiData.clientName ?: "", + Res.string.string_and_string, + stringResource(Res.string.new_loan_application) + " ", + state.loanWithAssociations.clientName.toString(), ) } else { - stringResource(id = R.string.loan_name) + stringResource(Res.string.loan_name) }, - color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) - Text( style = MaterialTheme.typography.bodyMedium, text = stringResource( - id = R.string.string_and_string, - stringResource(R.string.account_number) + " ", - uiData.accountNumber ?: "", + Res.string.string_and_string, + stringResource(Res.string.account_number) + " ", + state.loanWithAssociations?.accountNo ?: "", ), - color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth(), ) - Spacer(modifier = Modifier.height(16.dp)) - MifosDropDownTextField( - optionsList = uiData.listLoanProducts.filterNotNull(), - selectedOption = uiData.selectedLoanProduct, + optionsList = state.listLoanProducts.filterNotNull(), + selectedOption = state.selectedLoanProduct, supportingText = selectedLoanProductError ?: "", error = showSelectedLoanProductError, - labelResId = R.string.select_loan_product, + labelResId = Res.string.select_loan_product, onClick = { position, item -> selectProduct(position) selectedLoanProduct = item @@ -150,60 +173,61 @@ internal fun LoanApplicationContent( }, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosDropDownTextField( - optionsList = uiData.listLoanPurpose.filterNotNull(), - selectedOption = uiData.selectedLoanPurpose, + optionsList = state.listLoanPurpose.filterNotNull(), + selectedOption = state.selectedLoanPurpose, isEnabled = purposeTextFieldEnable, - labelResId = R.string.purpose_of_loan, + labelResId = Res.string.purpose_of_loan, onClick = { index, item -> selectPurpose(index) selectedLoanPurpose = item }, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosOutlinedTextField( - value = principalAmount, - onValueChange = { principalAmount = it }, - label = R.string.principal_amount, - error = showPrincipalAmountError, + value = principalAmount.text, + onValueChange = { principalAmount = TextFieldValue(it) }, + label = stringResource(Res.string.principal_amount), modifier = Modifier.fillMaxWidth(), - supportingText = principalAmountError ?: "", - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number, + config = MifosTextFieldConfig( + errorText = principalAmountError ?: "", + isError = showPrincipalAmountError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + ), ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.currency), - description = uiData.currencyLabel ?: "", + title = stringResource(Res.string.currency), + description = state.loanWithAssociations?.currency?.displaySymbol ?: "", ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.submission_date), - description = uiData.submittedDate ?: "", + title = stringResource(Res.string.submission_date), + description = state.loanWithAssociations?.timeline?.submittedOnDate?.let { + DateHelper.getDateAsString( + it, + ) + } ?: "", ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDrawableSingleLine( - title = stringResource(id = R.string.expected_disbursement_date), - description = expectedDisbursementDate ?: "", - imageResId = R.drawable.ic_edit_black_24dp, + title = stringResource(Res.string.expected_disbursement_date), + description = expectedDisbursementDate?.let { + DateHelper.getDateAsString( + it, + ) + } ?: "", + imageResId = Res.drawable.ic_edit_black_24dp, imageSize = 24.dp, onDrawableClick = { showDatePicker = true }, ) - Spacer(modifier = Modifier.height(20.dp)) - MifosButton( - textResId = R.string.review, + text = { stringResource(Res.string.review) }, + modifier = modifier.fillMaxWidth(), onClick = { when { selectedLoanProductError != null -> showSelectedLoanProductError = true @@ -211,7 +235,6 @@ internal fun LoanApplicationContent( else -> reviewClicked(principalAmount.text) } }, - modifier = Modifier.fillMaxWidth(), ) } @@ -222,47 +245,44 @@ internal fun LoanApplicationContent( confirmButton = { MifosTextButton( onClick = { - val formattedDate = DateHelper.getSpecificFormat( - format = FORMAT_MM, - dateLong = datePickerState.selectedDateMillis, - ) + val formattedDate = datePickerState.selectedDateMillis?.let { millis -> + val instant = Instant.fromEpochMilliseconds(millis) + val localDate = instant.toLocalDateTime(TimeZone.currentSystemDefault()).date + DateHelper.getSpecificFormat( + DateHelper.MONTH_FORMAT, + localDate.format(DateHelper.SHORT_MONTH), + ) + } formattedDate?.let { - expectedDisbursementDate = formattedDate - setDisbursementDate(formattedDate) + val dateList = DateHelper.getDateAsList(it) + expectedDisbursementDate = dateList + setDisbursementDate(it) } showDatePicker = false }, - text = stringResource(id = R.string.dialog_action_ok), + text = { stringResource(Res.string.dialog_action_ok) }, ) }, dismissButton = { MifosTextButton( onClick = { showDatePicker = false }, - text = stringResource(id = R.string.dialog_action_cancel), + text = { stringResource(Res.string.dialog_action_cancel) }, ) }, - colors = DatePickerDefaults.colors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) { DatePicker( state = datePickerState, - colors = DatePickerDefaults.colors( - todayContentColor = MaterialTheme.colorScheme.primary, - todayDateBorderColor = MaterialTheme.colorScheme.primary, - selectedDayContainerColor = MaterialTheme.colorScheme.primary, - ), ) } } } -@DevicePreviews +@Preview @Composable private fun LoanAccountApplicationContentPreview() { MifosMobileTheme { LoanApplicationContent( - uiData = LoanApplicationScreenData(), + state = LoanApplicationState(dialogState = null), selectProduct = { }, selectPurpose = { }, reviewClicked = { }, diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt new file mode 100644 index 000000000..53ab6a992 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountApplication + +import androidx.compose.foundation.layout.Box +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.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.apply_for_loan +import mifos_mobile.feature.loan.generated.resources.update_loan +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.designsystem.component.MifosTopBar +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme +import org.mifos.mobile.core.model.enums.LoanState +import org.mifos.mobile.core.ui.component.MifosErrorComponent +import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.utils.EventsEffect + +@Composable +internal fun LoanApplicationScreen( + navigateBack: () -> Unit, + reviewNewLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit, + submitUpdateLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit, + modifier: Modifier = Modifier, + viewModel: LoanApplicationViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + LoanApplicationEvent.NavigateBack -> navigateBack.invoke() + is LoanApplicationEvent.ReviewLoanApplication -> { + reviewNewLoanApplication( + event.loanState, + event.loansPayloadString, + event.loanId, + event.loanName, + event.accountNo, + ) + } + is LoanApplicationEvent.SubmitUpdateLoanApplication -> { + submitUpdateLoanApplication( + event.loanState, + event.loansPayloadString, + event.loanId, + event.loanName, + event.accountNo, + ) + } + } + } + + LoanApplicationDialog( + dialogState = state.dialogState, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + state = state, + ) + + LoanApplicationScreen( + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + modifier = modifier, + + ) +} + +@Composable +private fun LoanApplicationDialog( + dialogState: LoanApplicationState.DialogState?, + onAction: (LoanApplicationAction) -> Unit, + state: LoanApplicationState, +) { + when (dialogState) { + is LoanApplicationState.DialogState.Error -> MifosErrorComponent( + isNetworkConnected = state.isOnline, + isEmptyData = false, + isRetryEnabled = true, + onRetry = { onAction(LoanApplicationAction.Retry) }, + ) + LoanApplicationState.DialogState.Loading -> MifosProgressIndicator(modifier = Modifier.fillMaxSize()) + null -> Unit + } +} + +@Composable +private fun LoanApplicationScreen( + state: LoanApplicationState, + modifier: Modifier = Modifier, + onAction: (LoanApplicationAction) -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + MifosTopBar( + modifier = Modifier.fillMaxWidth(), + backPress = { onAction(LoanApplicationAction.BackPress) }, + topBarTitle = + stringResource( + if (state.loanState == LoanState.CREATE) { + Res.string.apply_for_loan + } else { + Res.string.update_loan + }, + ), + ) + }, + content = { + Column( + modifier = Modifier + .padding(it) + .fillMaxSize(), + ) { + Box(modifier = Modifier.weight(1f)) { + LoanApplicationContent( + state = state, + selectProduct = { position -> + (onAction(LoanApplicationAction.ProductSelected(position))) + }, + selectPurpose = { position -> + onAction(LoanApplicationAction.PurposeSelected(position)) + }, + reviewClicked = { principalAmount -> + onAction(LoanApplicationAction.SetPrincipalAmount(principalAmount)) + + onAction( + LoanApplicationAction.ReviewClicked + (state.reviewNewLoanApplication, state.submitUpdateLoanApplication), + ) + }, + setDisbursementDate = { data -> + onAction(LoanApplicationAction.SetDisburseDate(data)) + }, + ) + } + } + }, + ) +} + +@Preview +@Composable +private fun ReviewLoanApplicationScreenPreview() { + MifosMobileTheme { + LoanApplicationScreen( + state = LoanApplicationState(dialogState = null), + onAction = {}, + modifier = Modifier, + + ) + } +} diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt new file mode 100644 index 000000000..4910c4070 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt @@ -0,0 +1,471 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountApplication + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_number +import mifos_mobile.feature.loan.generated.resources.error_fetching_template +import mifos_mobile.feature.loan.generated.resources.new_loan_application +import mifos_mobile.feature.loan.generated.resources.string_and_string +import mifos_mobile.feature.loan.generated.resources.update_loan_application +import org.jetbrains.compose.resources.getString +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.common.DateHelper +import org.mifos.mobile.core.common.formatAmount +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.datastore.UserPreferencesRepository +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.model.entity.payload.LoansPayload +import org.mifos.mobile.core.model.entity.templates.loans.LoanTemplate +import org.mifos.mobile.core.model.enums.LoanState +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanApplicationViewModel( + private val loanRepositoryImp: LoanRepository, + private val networkMonitor: NetworkMonitor, + userPreferencesRepositoryImpl: UserPreferencesRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = LoanApplicationState( + dialogState = null, + clientId = requireNotNull(userPreferencesRepositoryImpl.clientId.value), + loanId = savedStateHandle.getStateFlow( + key = Constants.LOAN_ID, + initialValue = null, + ).value, + loanState = savedStateHandle.getStateFlow( + key = Constants.LOAN_STATE, + initialValue = LoanState.CREATE.name, + ).value.let { LoanState.valueOf(it) }, + + ), +) { + + private val isLoanUpdatePurposesInitialization: Boolean = true + + init { + viewModelScope.launch { + networkMonitor.isOnline.collect { isOnline -> + updateState { it.copy(isOnline = isOnline) } + } + } + + loadLoanWithAssociations() + } + + private fun updateState(update: (LoanApplicationState) -> LoanApplicationState) { + mutableStateFlow.update(update) + } + + override fun handleAction(action: LoanApplicationAction) { + when (action) { + is LoanApplicationAction.ProductSelected -> productSelected(action.position) + is LoanApplicationAction.PurposeSelected -> purposeSelected(action.position) + is LoanApplicationAction.SetDisburseDate -> setDisburseDate(action.date) + is LoanApplicationAction.SetPrincipalAmount -> setPrincipalAmount(action.amount) + is LoanApplicationAction.ReviewClicked -> { + handleReviewClicked() + } + LoanApplicationAction.BackPress -> sendEvent(LoanApplicationEvent.NavigateBack) + LoanApplicationAction.Retry -> loadLoanApplicationTemplate(state.loanState) + } + } + + private fun loadLoanWithAssociations() { + updateState { it.copy(dialogState = LoanApplicationState.DialogState.Loading) } + viewModelScope.launch { + loanRepositoryImp.getLoanWithAssociations(Constants.TRANSACTIONS, state.loanId) + .catch { + updateState { + it.copy( + dialogState = LoanApplicationState.DialogState.Error + ("An error occurred"), + ) + } + }.collect { dataState -> + when (dataState) { + DataState.Loading -> updateState { + it.copy(dialogState = LoanApplicationState.DialogState.Loading) + } + is DataState.Success -> { + updateState { + it.copy( + loanWithAssociations = dataState.data, + dialogState = null, + ) + } + } + is DataState.Error -> { + updateState { + it.copy( + dialogState = LoanApplicationState.DialogState + .Error(dataState.message), + ) + } + } + } + } + } + } + + private fun loadLoanApplicationTemplate(loanState: LoanState) { + viewModelScope.launch { + val errorMessage = getString(Res.string.error_fetching_template) + loanRepositoryImp.template(state.clientId) + .collect { result -> + when (result) { + is DataState.Loading -> { + updateState { it.copy(dialogState = LoanApplicationState.DialogState.Loading) } + } + is DataState.Success -> { + val loanTemplate = result.data ?: LoanTemplate() + updateState { it.copy(loanTemplate = loanTemplate) } + if (loanState == LoanState.CREATE) { + showLoanTemplate(loanTemplate) + } else { + showUpdateLoanTemplate(loanTemplate) + } + } + is DataState.Error -> { + updateState { + it.copy( + dialogState = LoanApplicationState.DialogState + .Error(errorMessage), + ) + } + } + } + } + } + } + + private fun showLoanTemplate(loanTemplate: LoanTemplate) { + val listLoanProducts = refreshLoanProductList(loanTemplate) + updateState { + it.copy( + listLoanProducts = listLoanProducts, + selectedLoanProduct = listLoanProducts.firstOrNull(), + accountNumber = loanTemplate.clientAccountNo, + clientName = loanTemplate.clientName, + currencyLabel = loanTemplate.currency?.displayLabel, + principalAmount = formatAmount(loanTemplate.principal ?: 0.0), + disbursementDate = DateHelper.formattedFullDate, + submittedDate = DateHelper.formattedFullDate, + ) + } + } + + private fun showUpdateLoanTemplate(loanTemplate: LoanTemplate) { + val listLoanProducts = refreshLoanProductList(loanTemplate) + updateState { + it.copy( + listLoanProducts = listLoanProducts, + selectedLoanProduct = state.loanWithAssociations?.loanProductName, + accountNumber = state.loanWithAssociations?.accountNo, + clientName = state.loanWithAssociations?.clientName, + currencyLabel = state.loanWithAssociations?.currency?.displayLabel, + principalAmount = formatAmount(state.loanWithAssociations?.principal ?: 0.0), + submittedDate = state.loanWithAssociations?.timeline?.submittedOnDate + ?.map { date -> date.toLong() } + ?.let { date -> DateHelper.getDateAsString(date, "dd-MM-yyyy") }, + disbursementDate = state.loanWithAssociations?.timeline?.expectedDisbursementDate + ?.map { date -> date.toLong() } + ?.let { date -> DateHelper.getDateAsString(date, "dd-MM-yyyy") }, + + ) + } + } + + private fun refreshLoanProductList(loanTemplate: LoanTemplate): List { + val loanProductList = state.listLoanProducts.toMutableList() + for ((_, name) in loanTemplate.productOptions) { + if (!loanProductList.contains(name)) { + loanProductList.add(name) + } + } + return loanProductList + } + + private fun productSelected(position: Int) { + val selectedProduct = state.listLoanProducts.getOrNull(position) + updateState { it.copy(selectedLoanProduct = selectedProduct) } + loadLoanApplicationTemplateByProduct(position, state.loanState) + } + + private fun loadLoanApplicationTemplateByProduct(position: Int, loanState: LoanState) { + val productId = state.loanTemplate?.productOptions?.get(position)?.id ?: return + viewModelScope.launch { + val errorMessage = getString(Res.string.error_fetching_template) + loanRepositoryImp.getLoanTemplateByProduct( + clientId = state.clientId, + productId = + productId, + ) + .collect { result -> + when (result) { + is DataState.Loading -> { + updateState { it.copy(dialogState = LoanApplicationState.DialogState.Loading) } + } + is DataState.Success -> { + result.data?.let { + if (loanState == LoanState.CREATE) { + showLoanTemplateByProduct(loanTemplate = it) + } else { + showUpdateLoanTemplateByProduct(loanTemplate = it) + } + } + } + is DataState.Error -> { + updateState { + it.copy( + dialogState = LoanApplicationState.DialogState + .Error(errorMessage), + ) + } + } + } + } + } + } + + private fun showLoanTemplateByProduct(loanTemplate: LoanTemplate) { + val loanPurposeList = refreshLoanPurposeList(loanTemplate) + updateState { + it.copy( + listLoanPurpose = loanPurposeList, + selectedLoanPurpose = loanPurposeList.firstOrNull(), + accountNumber = loanTemplate.clientAccountNo, + clientName = loanTemplate.clientName, + currencyLabel = loanTemplate.currency?.displayLabel, + principalAmount = formatAmount(loanTemplate.principal ?: 0.0), + ) + } + } + + private fun showUpdateLoanTemplateByProduct(loanTemplate: LoanTemplate) { + val loanPurposeList = refreshLoanPurposeList(loanTemplate = loanTemplate) + if (isLoanUpdatePurposesInitialization && state.loanWithAssociations?.loanPurposeName != null) { + updateState { + it.copy( + listLoanPurpose = loanPurposeList, + selectedLoanPurpose = loanPurposeList[0], + ) + } + } else { + updateState { + it.copy( + listLoanPurpose = loanPurposeList, + selectedLoanPurpose = state.loanWithAssociations?.loanPurposeName, + accountNumber = loanTemplate.clientAccountNo, + clientName = loanTemplate.clientName, + currencyLabel = loanTemplate.currency?.displayLabel, + principalAmount = formatAmount(loanTemplate.principal ?: 0.0), + ) + } + } + } + + private fun refreshLoanPurposeList(loanTemplate: LoanTemplate): MutableList { + val loanPurposeList = mutableListOf() + loanPurposeList.add("Purpose not provided") + for (loanPurposeOptions in loanTemplate.loanPurposeOptions) { + loanPurposeList.add(loanPurposeOptions.name) + } + return loanPurposeList + } + + private fun purposeSelected(position: Int) { + val selectedPurposeId = state.listLoanPurpose.getOrNull(position) + if (selectedPurposeId != null) { + updateState { it.copy(loanPurposeId = selectedPurposeId.toInt()) } + } + } + + private fun setDisburseDate(date: String) { + updateState { it.copy(disbursementDate = date) } + } + + private fun setPrincipalAmount(amount: String) { + updateState { it.copy(principalAmount = amount) } + } + + private fun getLoanPayload(): String { + val payload = LoansPayload( + clientId = state.loanTemplate?.clientId.takeIf { state.loanState == LoanState.CREATE }, + loanPurpose = state.selectedLoanPurpose ?: "Not provided", + productName = state.selectedLoanProduct, + currency = state.currencyLabel, + loanPurposeId = if (state.loanPurposeId!! > 0) state.loanPurposeId else null, + productId = state.loanWithAssociations?.loanProductId, + principal = state.principalAmount?.toDoubleOrNull() ?: 0.0, + loanTermFrequency = state.loanTemplate?.termFrequency, + loanTermFrequencyType = state.loanTemplate?.interestRateFrequencyType?.id, + loanType = "individual".takeIf { state.loanState == LoanState.CREATE }, + numberOfRepayments = state.loanTemplate?.numberOfRepayments, + repaymentEvery = state.loanTemplate?.repaymentEvery, + repaymentFrequencyType = state.loanTemplate?.interestRateFrequencyType?.id, + interestRatePerPeriod = state.loanTemplate?.interestRatePerPeriod, + expectedDisbursementDate = state.disbursementDate?.let { + DateHelper.getSpecificFormat(DateHelper.MONTH_FORMAT, it) + }, + submittedOnDate = state.submittedDate?.let { + DateHelper.getSpecificFormat(DateHelper.MONTH_FORMAT, it) + .takeIf { state.loanState == LoanState.CREATE } + }, + transactionProcessingStrategyId = state.loanTemplate?.transactionProcessingStrategyId, + amortizationType = state.loanTemplate?.amortizationType?.id, + interestCalculationPeriodType = state.loanTemplate?.interestCalculationPeriodType?.id, + interestType = state.loanTemplate?.interestType?.id, + ) + + val loansPayloadString = Json.encodeToString(payload) + return loansPayloadString + } + + private fun handleReviewClicked() { + viewModelScope.launch { + val payload = getLoanPayload() + val event = when (state.loanState) { + LoanState.CREATE -> LoanApplicationEvent.ReviewLoanApplication( + state.loanState, + payload, + state.loanId, + getString( + Res.string.string_and_string, + getString(Res.string.new_loan_application), + state.loanWithAssociations?.clientName ?: "", + ), + getString( + Res.string.string_and_string, + getString(Res.string.account_number), + state.loanWithAssociations?.accountNo ?: "", + ), + ) + LoanState.UPDATE -> LoanApplicationEvent.SubmitUpdateLoanApplication( + state.loanState, + payload, + null, + getString( + Res.string.string_and_string, + getString(Res.string.update_loan_application), + state.loanWithAssociations?.clientName ?: "", + ), + getString( + Res.string.string_and_string, + getString(Res.string.account_number) + " ", + state.loanWithAssociations?.accountNo ?: "", + ), + ) + } + sendEvent(event) + } + } +} + +@Parcelize +data class LoanApplicationState( + val clientId: Long? = null, + val isOnline: Boolean = false, + val loanState: LoanState = LoanState.CREATE, + val loanId: Long? = null, + val loanWithAssociations: LoanWithAssociations? = null, + val loanTemplate: LoanTemplate? = null, +// val isLoading: Boolean = false, + val errorMessage: String? = null, + val listLoanProducts: List = listOf(), + val selectedLoanProduct: String? = null, + val listLoanPurpose: List = listOf(), + val selectedLoanPurpose: String? = null, + val loanPurposeId: Int? = null, + val principalAmount: String? = null, + val currencyLabel: String? = null, + val accountNumber: String? = null, + val clientName: String? = null, + val disbursementDate: String? = null, + val submittedDate: String? = null, + val reviewNewLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit = { _, _, _, _, _ -> }, + val submitUpdateLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit = { _, _, _, _, _ -> }, + val dialogState: DialogState?, +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface LoanApplicationEvent { + data object NavigateBack : LoanApplicationEvent + data class ReviewLoanApplication( + val loanState: LoanState, + val loansPayloadString: String, + val loanId: Long?, + val loanName: String, + val accountNo: String, + ) : LoanApplicationEvent + + data class SubmitUpdateLoanApplication( + val loanState: LoanState, + val loansPayloadString: String, + val loanId: Long?, + val loanName: String, + val accountNo: String, + ) : LoanApplicationEvent +} + +sealed interface LoanApplicationAction { + data class ProductSelected(val position: Int) : LoanApplicationAction + data class PurposeSelected(val position: Int) : LoanApplicationAction + data class SetDisburseDate(val date: String) : LoanApplicationAction + data class SetPrincipalAmount(val amount: String) : LoanApplicationAction + data object BackPress : LoanApplicationAction + data object Retry : LoanApplicationAction + data class ReviewClicked( + val reviewNewLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit, + val submitUpdateLoanApplication: ( + loanState: LoanState, + loansPayloadString: String, + loanId: Long?, + loanName: String, + accountNo: String, + ) -> Unit, + ) : LoanApplicationAction +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt similarity index 55% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt index e19ffadd8..92499c7ca 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryScreen.kt @@ -9,6 +9,7 @@ */ package org.mifos.mobile.feature.loan.loanAccountSummary +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -18,73 +19,102 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.designsystem.components.MifosScaffold +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_short +import mifos_mobile.feature.loan.generated.resources.account_status +import mifos_mobile.feature.loan.generated.resources.active_uc +import mifos_mobile.feature.loan.generated.resources.fees +import mifos_mobile.feature.loan.generated.resources.fees_waived +import mifos_mobile.feature.loan.generated.resources.ic_check_circle_green_24px +import mifos_mobile.feature.loan.generated.resources.ic_report_problem_red_24px +import mifos_mobile.feature.loan.generated.resources.inactive_uc +import mifos_mobile.feature.loan.generated.resources.interest +import mifos_mobile.feature.loan.generated.resources.interest_waived +import mifos_mobile.feature.loan.generated.resources.loan_product +import mifos_mobile.feature.loan.generated.resources.loan_summary +import mifos_mobile.feature.loan.generated.resources.outstanding_balance +import mifos_mobile.feature.loan.generated.resources.penalties +import mifos_mobile.feature.loan.generated.resources.penalties_waived +import mifos_mobile.feature.loan.generated.resources.principal +import mifos_mobile.feature.loan.generated.resources.string_and_double +import mifos_mobile.feature.loan.generated.resources.total_paid +import mifos_mobile.feature.loan.generated.resources.total_repayment +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.designsystem.component.MifosCard +import org.mifos.mobile.core.designsystem.component.MifosScaffold import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay import org.mifos.mobile.core.ui.component.MifosTextTitleDescDrawableSingleLine import org.mifos.mobile.core.ui.component.MifosTextTitleDescSingleLine -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R +import org.mifos.mobile.core.ui.utils.EventsEffect @Composable internal fun LoanAccountSummaryScreen( navigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: LoanAccountSummaryViewModel = hiltViewModel(), + viewModel: LoanAccountSummaryViewModel = koinViewModel(), ) { - val uiState by viewModel.loanUiState.collectAsStateWithLifecycle() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + LoanAccountSummaryEvent.NavigateBack -> navigateBack.invoke() + } + } + + LoanAccountSummaryDialog( + dialogState = state.dialogState, + state = state, + ) LoanAccountSummaryScreen( - uiState = uiState, - navigateBack = navigateBack, + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, modifier = modifier, ) } +@Composable +private fun LoanAccountSummaryDialog( + dialogState: LoanAccountSummaryState.DialogState?, + state: LoanAccountSummaryState, +) { + when (dialogState) { + is LoanAccountSummaryState.DialogState.Error -> MifosErrorComponent(isNetworkConnected = state.isOnline) + LoanAccountSummaryState.DialogState.Loading -> MifosProgressIndicatorOverlay() + null -> Unit + } +} + @Composable private fun LoanAccountSummaryScreen( - uiState: LoanAccountSummaryUiState, - navigateBack: () -> Unit, + state: LoanAccountSummaryState, modifier: Modifier = Modifier, + onAction: (LoanAccountSummaryAction) -> Unit, ) { - val context = LocalContext.current - MifosScaffold( - topBarTitleResId = R.string.loan_summary, - navigateBack = navigateBack, - modifier = modifier, - content = { - Box(modifier = Modifier.padding(it)) { - when (uiState) { - is LoanAccountSummaryUiState.Loading -> { - MifosProgressIndicatorOverlay() - } - - is LoanAccountSummaryUiState.Error -> { - MifosErrorComponent(isNetworkConnected = Network.isConnected(context)) - } - - is LoanAccountSummaryUiState.Success -> { - LoanAccountSummaryContent( - loanWithAssociations = uiState.loanWithAssociations, - ) - } - } - } - }, - ) + topBarTitle = stringResource(Res.string.loan_summary), + backPress = { (onAction(LoanAccountSummaryAction.BackPress)) }, + ) { + Box(modifier = Modifier.padding(it)) { + LoanAccountSummaryContent( + loanWithAssociations = state.loanAccountAssociations, + modifier = modifier, + ) + } + } } @Composable @@ -101,22 +131,23 @@ private fun LoanAccountSummaryContent( modifier = modifier .fillMaxSize() .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { MifosTextTitleDescSingleLine( modifier = Modifier.padding(horizontal = 14.dp), - title = stringResource(id = R.string.account_short), + title = stringResource(Res.string.account_short), description = loanWithAssociations?.accountNo ?: "", ) MifosTextTitleDescSingleLine( modifier = Modifier.padding(horizontal = 14.dp), - title = stringResource(id = R.string.loan_product), + title = stringResource(Res.string.loan_product), description = loanWithAssociations?.loanProductName ?: "", ) - Spacer(modifier = Modifier.height(30.dp)) + Spacer(modifier = Modifier.height(8.dp)) - OutlinedCard( + MifosCard( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.outlinedCardColors( containerColor = MaterialTheme.colorScheme.background, @@ -128,36 +159,36 @@ private fun LoanAccountSummaryContent( .padding(14.dp), ) { MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.principal), + title = stringResource(Res.string.principal), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.principal ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.interest), + title = stringResource(Res.string.interest), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.interestCharged ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.fees), + title = stringResource(Res.string.fees), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.feeChargesCharged ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.penalties), + title = stringResource(Res.string.penalties), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.penaltyChargesCharged ?: 0.0, ), @@ -165,59 +196,57 @@ private fun LoanAccountSummaryContent( } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) - OutlinedCard( + MifosCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.background, - ), ) { Column( modifier = Modifier .fillMaxWidth() .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.total_repayment), + title = stringResource(Res.string.total_repayment), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.totalExpectedRepayment ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.total_paid), + title = stringResource(Res.string.total_paid), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.totalRepayment ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.interest_waived), + title = stringResource(Res.string.interest_waived), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.interestWaived ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.penalties_waived), + title = stringResource(Res.string.penalties_waived), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.penaltyChargesWaived ?: 0.0, ), ) MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.fees_waived), + title = stringResource(Res.string.fees_waived), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.feeChargesWaived ?: 0.0, ), @@ -225,13 +254,10 @@ private fun LoanAccountSummaryContent( } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) - OutlinedCard( + MifosCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.background, - ), ) { Column( modifier = Modifier @@ -239,26 +265,26 @@ private fun LoanAccountSummaryContent( .padding(14.dp), ) { MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.outstanding_balance), + title = stringResource(Res.string.outstanding_balance), description = stringResource( - id = R.string.string_and_double, + Res.string.string_and_double, currencySymbol, loanWithAssociations?.summary?.totalOutstanding ?: 0.0, ), ) MifosTextTitleDescDrawableSingleLine( - title = stringResource(id = R.string.account_status), + title = stringResource(Res.string.account_status), description = if (loanWithAssociations?.status?.active == true) { stringResource( - id = R.string.active_uc, + Res.string.active_uc, ) } else { - stringResource(id = R.string.inactive_uc) + stringResource(Res.string.inactive_uc) }, imageResId = if (loanWithAssociations?.status?.active == true) { - R.drawable.ic_check_circle_green_24px + Res.drawable.ic_check_circle_green_24px } else { - R.drawable.ic_report_problem_red_24px + Res.drawable.ic_report_problem_red_24px }, ) } @@ -266,13 +292,14 @@ private fun LoanAccountSummaryContent( } } -@DevicePreviews +@Preview @Composable private fun LoanAccountSummaryPreview() { MifosMobileTheme { LoanAccountSummaryScreen( - navigateBack = {}, - uiState = LoanAccountSummaryUiState.Success(loanWithAssociations = null), + state = LoanAccountSummaryState(dialogState = null), + onAction = {}, + modifier = Modifier, ) } } diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt new file mode 100644 index 000000000..499fd0213 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountSummary + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanAccountSummaryViewModel( + private val loanRepositoryImp: LoanRepository, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = LoanAccountSummaryState( + dialogState = null, + loanId = savedStateHandle.getStateFlow(Constants.LOAN_ID, null).value, + ), +) { + + init { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + updateState { it.copy(isOnline = isConnected) } + } + } + loadLoanAccountSummary() + } + + override fun handleAction(action: LoanAccountSummaryAction) { + when (action) { + is LoanAccountSummaryAction.LoanIdChanged -> { + updateState { it.copy(loanId = action.loanId) } + } + + LoanAccountSummaryAction.BackPress -> { + sendEvent(LoanAccountSummaryEvent.NavigateBack) + } + + is LoanAccountSummaryAction.Internal.ReceiveLoanSummaryResult -> { + handleLoanSummaryResult(action) + } + } + } + + private fun updateState(update: (LoanAccountSummaryState) -> LoanAccountSummaryState) { + mutableStateFlow.update(update) + } + + private fun loadLoanAccountSummary() { + viewModelScope.launch { + updateState { it.copy(dialogState = LoanAccountSummaryState.DialogState.Loading) } + + loanRepositoryImp.getLoanWithAssociations(Constants.REPAYMENT_SCHEDULE, state.loanId) + .catch { exception -> + sendAction( + LoanAccountSummaryAction.Internal.ReceiveLoanSummaryResult( + DataState.Error(exception), + ), + ) + } + .collect { result -> + sendAction(LoanAccountSummaryAction.Internal.ReceiveLoanSummaryResult(result)) + } + } + } + + private fun handleLoanSummaryResult(action: LoanAccountSummaryAction.Internal.ReceiveLoanSummaryResult) { + updateState { + when (val result = action.loanSummaryResult) { + is DataState.Error -> it.copy( + dialogState = LoanAccountSummaryState.DialogState.Error( + result.exception.message ?: "An error occurred", + ), + ) + + is DataState.Loading -> it.copy(dialogState = LoanAccountSummaryState.DialogState.Loading) + + is DataState.Success -> { + val loan = result.data + if (loan == null) { + it.copy(dialogState = LoanAccountSummaryState.DialogState.Error("Loan details not found")) + } else { + it.copy(loanAccountAssociations = loan, dialogState = null) + } + } + } + } + } +} + +@Parcelize +data class LoanAccountSummaryState( + val loanId: Long? = null, + val dialogState: DialogState?, + val loanAccountAssociations: LoanWithAssociations? = null, + val isOnline: Boolean = false, +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface LoanAccountSummaryEvent { + data object NavigateBack : LoanAccountSummaryEvent +} + +sealed interface LoanAccountSummaryAction { + data class LoanIdChanged(val loanId: Long) : LoanAccountSummaryAction + data object BackPress : LoanAccountSummaryAction + + sealed class Internal : LoanAccountSummaryAction { + data class ReceiveLoanSummaryResult( + val loanSummaryResult: DataState, + ) : Internal() + } +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt similarity index 50% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt index 1d36b03af..86ae03b73 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionScreen.kt @@ -10,12 +10,12 @@ package org.mifos.mobile.feature.loan.loanAccountTransaction import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -25,88 +25,92 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.common.utils.CurrencyUtil -import org.mifos.mobile.core.common.utils.DateHelper.getDateAsString -import org.mifos.mobile.core.common.utils.Utils.formatTransactionType -import org.mifos.mobile.core.designsystem.components.MifosTopBar +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.atm_icon +import mifos_mobile.feature.loan.generated.resources.ic_local_atm_black_24dp +import mifos_mobile.feature.loan.generated.resources.string_and_string +import mifos_mobile.feature.loan.generated.resources.transactions +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.common.CurrencyFormatter +import org.mifos.mobile.core.common.Utils.formatTransactionType +import org.mifos.mobile.core.designsystem.component.MifosTopBar import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.model.entity.Transaction import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R +import org.mifos.mobile.core.ui.utils.EventsEffect @Composable internal fun LoanAccountTransactionScreen( navigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: LoanAccountTransactionViewModel = hiltViewModel(), + viewModel: LoanAccountTransactionViewModel = koinViewModel(), ) { - val uiState by viewModel.loanUiState.collectAsStateWithLifecycle() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + LoanAccountTransactionEvent.NavigateBack -> navigateBack.invoke() + } + } + + LoanAccountTransactionDialog( + dialogState = state.dialogState, + state = state, + ) LoanAccountTransactionScreen( - uiState = uiState, - navigateBack = navigateBack, + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, modifier = modifier, ) } +@Composable +private fun LoanAccountTransactionDialog( + dialogState: LoanAccountTransactionState.DialogState?, + state: LoanAccountTransactionState, +) { + when (dialogState) { + is LoanAccountTransactionState.DialogState.Error -> { + MifosErrorComponent( + isNetworkConnected = state.isOnline, + isEmptyData = false, + ) + } + LoanAccountTransactionState.DialogState.Loading -> MifosProgressIndicatorOverlay() + null -> Unit + } +} + @Composable private fun LoanAccountTransactionScreen( - uiState: LoanAccountTransactionUiState, - navigateBack: () -> Unit, + state: LoanAccountTransactionState, modifier: Modifier = Modifier, + onAction: (LoanAccountTransactionAction) -> Unit, ) { - val context = LocalContext.current - var loanWithAssociations by rememberSaveable { mutableStateOf(LoanWithAssociations()) } - - Column(modifier = modifier.fillMaxSize()) { + Column(modifier = modifier.fillMaxSize().padding(16.dp)) { MifosTopBar( - navigateBack = navigateBack, - title = { Text(text = stringResource(id = R.string.transactions)) }, + backPress = { (onAction(LoanAccountTransactionAction.BackPress)) }, + topBarTitle = stringResource(Res.string.transactions), ) Box(modifier = Modifier.weight(1f)) { - LoanAccountTransactionContent(loanWithAssociations = loanWithAssociations) - - when (uiState) { - is LoanAccountTransactionUiState.Success -> { - if (uiState.loanWithAssociations != null && - uiState.loanWithAssociations.transactions?.isNotEmpty() == true - ) { - loanWithAssociations = uiState.loanWithAssociations - } else { - MifosErrorComponent(isEmptyData = true) - } - } - - is LoanAccountTransactionUiState.Loading -> { - MifosProgressIndicatorOverlay() - } - - is LoanAccountTransactionUiState.Error -> { - MifosErrorComponent( - isNetworkConnected = Network.isConnected(context), - isEmptyData = false, - ) - } + state.loanWithAssociations?.let { + LoanAccountTransactionContent(loanWithAssociations = it) } } } @@ -117,19 +121,21 @@ private fun LoanAccountTransactionContent( loanWithAssociations: LoanWithAssociations, modifier: Modifier = Modifier, ) { + var currencySymbol = loanWithAssociations.currency?.displaySymbol + if (currencySymbol == null) { + currencySymbol = loanWithAssociations.currency?.code ?: "" + } Column( modifier = modifier .fillMaxSize() .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( text = loanWithAssociations.loanProductName ?: "", style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface, ) - Spacer(modifier = Modifier.height(8.dp)) - LazyColumn { items(items = loanWithAssociations.transactions?.toList().orEmpty()) { LoanAccountTransactionListItem(it) @@ -143,15 +149,13 @@ private fun LoanAccountTransactionListItem( transaction: Transaction?, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - Row( modifier = modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( - painter = painterResource(id = R.drawable.ic_local_atm_black_24dp), - contentDescription = stringResource(id = R.string.atm_icon), + painter = painterResource(Res.drawable.ic_local_atm_black_24dp), + contentDescription = stringResource(Res.string.atm_icon), modifier = Modifier.size(39.dp), ) @@ -161,57 +165,42 @@ private fun LoanAccountTransactionListItem( Text( text = formatTransactionType(transaction?.type?.value), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, ) Row { Text( text = stringResource( - id = R.string.string_and_string, + Res.string.string_and_string, transaction?.currency?.displaySymbol ?: transaction?.currency?.code ?: "", - CurrencyUtil.formatCurrency( - context = context, + CurrencyFormatter.format( transaction?.amount ?: 0.0, + "", + 5, ), ), style = MaterialTheme.typography.labelMedium, modifier = Modifier .weight(1f) .alpha(0.7f), - color = MaterialTheme.colorScheme.onSurface, ) Text( - text = getDateAsString(transaction?.submittedOnDate), + text = transaction?.submittedOnDate.toString(), style = MaterialTheme.typography.labelMedium, modifier = Modifier.alpha(0.7f), - color = MaterialTheme.colorScheme.onSurface, ) } } } } -internal class LoanAccountTransactionUiStatesParameterProvider : - PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - LoanAccountTransactionUiState.Success(LoanWithAssociations()), - LoanAccountTransactionUiState.Error, - LoanAccountTransactionUiState.Error, - LoanAccountTransactionUiState.Loading, - ) -} - -@DevicePreviews +@Preview @Composable -private fun LoanAccountTransactionScreenPreview( - @PreviewParameter(LoanAccountTransactionUiStatesParameterProvider::class) - loanAccountTransactionUiState: LoanAccountTransactionUiState, -) { +private fun LoanAccountTransactionScreenPreview() { MifosMobileTheme { LoanAccountTransactionScreen( - uiState = loanAccountTransactionUiState, - navigateBack = {}, + state = LoanAccountTransactionState(dialogState = null), + modifier = Modifier, + onAction = {}, ) } } diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt new file mode 100644 index 000000000..614dc7eb3 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountTransaction + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanAccountTransactionViewModel( + private val loanRepositoryImp: LoanRepository, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel< + LoanAccountTransactionState, + LoanAccountTransactionEvent, + LoanAccountTransactionAction, + >( + initialState = LoanAccountTransactionState( + dialogState = null, + loanId = savedStateHandle.getStateFlow( + key = Constants.LOAN_ID, + initialValue = null, + ).value, + ), +) { + + init { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + updateState { it.copy(isOnline = isConnected) } + } + } + getLoanTransactionResult() + } + + private fun updateState(update: (LoanAccountTransactionState) -> LoanAccountTransactionState) { + mutableStateFlow.update(update) + } + + override fun handleAction(action: LoanAccountTransactionAction) { + when (action) { + LoanAccountTransactionAction.BackPress -> sendEvent(LoanAccountTransactionEvent.NavigateBack) + is LoanAccountTransactionAction.Internal.ReceiveLoanTransactionResult -> { + handleLoanTransactionResult(action) + } + } + } + + private fun getLoanTransactionResult() { + viewModelScope.launch { + loanRepositoryImp.getLoanWithAssociations( + Constants.TRANSACTIONS, + state + .loanId, + ).catch { exception -> + sendAction( + LoanAccountTransactionAction.Internal.ReceiveLoanTransactionResult( + DataState.Error(exception), + ), + ) + }.collect { result -> + sendAction(LoanAccountTransactionAction.Internal.ReceiveLoanTransactionResult(result)) + } + } + } + + private fun handleLoanTransactionResult( + action: LoanAccountTransactionAction.Internal + .ReceiveLoanTransactionResult, + ) { + updateState { + when (action.loanAccountsTransactionResult) { + is DataState.Error -> it.copy( + dialogState = LoanAccountTransactionState + .DialogState.Error(action.loanAccountsTransactionResult.message), + ) + is DataState.Loading -> it.copy( + dialogState = LoanAccountTransactionState + .DialogState.Loading, + ) + is DataState.Success -> it.copy( + loanWithAssociations = action + .loanAccountsTransactionResult.data, + dialogState = null, + ) + } + } + } +} + +@Parcelize +data class LoanAccountTransactionState( + val loanId: Long? = null, + val dialogState: DialogState?, + val loanWithAssociations: LoanWithAssociations? = null, + val isOnline: Boolean = false, +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface LoanAccountTransactionEvent { + data object NavigateBack : LoanAccountTransactionEvent +} + +sealed interface LoanAccountTransactionAction { + data object BackPress : LoanAccountTransactionAction + sealed class Internal : LoanAccountTransactionAction { + data class ReceiveLoanTransactionResult( + val loanAccountsTransactionResult: DataState, + ) : Internal() + } +} diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt new file mode 100644 index 000000000..a17269774 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountWithdraw + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_number +import mifos_mobile.feature.loan.generated.resources.client_name +import mifos_mobile.feature.loan.generated.resources.error_loan_account_withdraw +import mifos_mobile.feature.loan.generated.resources.withdraw_loan +import mifos_mobile.feature.loan.generated.resources.withdraw_loan_reason +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.designsystem.component.MifosButton +import org.mifos.mobile.core.designsystem.component.MifosTextField +import org.mifos.mobile.core.designsystem.component.MifosTopBar +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.ui.component.MifosErrorComponent +import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.component.MifosTitleDescSingleLineEqual +import org.mifos.mobile.core.ui.utils.EventsEffect + +@Composable +internal fun LoanAccountWithdrawScreen( + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LoanAccountWithdrawViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() + + val snackbarHostState = remember { SnackbarHostState() } + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + LoanAccountWithdrawEvent.NavigateBack -> navigateBack.invoke() + is LoanAccountWithdrawEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + LoanAccountWithDrawDialog( + dialogState = state.dialogState, + ) + + LoanAccountWithdrawScreen( + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + modifier = modifier, + ) +} + +@Composable +private fun LoanAccountWithDrawDialog( + dialogState: LoanAccountWithdrawState.DialogState?, +) { + when (dialogState) { + is LoanAccountWithdrawState.DialogState.Loading -> { + MifosProgressIndicator( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), + ) + } + + is LoanAccountWithdrawState.DialogState.Error -> { + MifosErrorComponent(message = stringResource(Res.string.error_loan_account_withdraw)) + } + null -> Unit + } +} + +@Composable +private fun LoanAccountWithdrawScreen( + state: LoanAccountWithdrawState, + onAction: (LoanAccountWithdrawAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + MifosTopBar( + backPress = { onAction(LoanAccountWithdrawAction.BackPress) }, + topBarTitle = stringResource(Res.string.withdraw_loan), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Box(modifier = Modifier.weight(1f).padding(16.dp)) { + LoanAccountWithdrawContent( + loanWithAssociations = state.loanWithAssociations, + state = state, + onAction = onAction, + ) + } + } +} + +@Composable +private fun LoanAccountWithdrawContent( + loanWithAssociations: LoanWithAssociations?, + state: LoanAccountWithdrawState, + onAction: (LoanAccountWithdrawAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + MifosTitleDescSingleLineEqual( + title = stringResource(Res.string.client_name), + description = loanWithAssociations?.clientName ?: "", + ) + + MifosTitleDescSingleLineEqual( + title = stringResource(Res.string.account_number), + description = loanWithAssociations?.accountNo ?: "", + ) + + MifosTextField( + modifier = Modifier.fillMaxWidth(), + value = state.loanReason, + label = stringResource(Res.string.withdraw_loan_reason), +// placeholder = { +// Text(text = stringResource(Res.string.withdraw_loan_reason)) +// }, + onValueChange = { onAction(LoanAccountWithdrawAction.LoanReasonChanged(it)) }, + textStyle = MaterialTheme.typography.bodyLarge, + ) + + MifosButton( + modifier = Modifier.fillMaxWidth(), + onClick = { onAction(LoanAccountWithdrawAction.WithDrawClicked) }, + content = { + Text( + text = stringResource(Res.string.withdraw_loan), + style = MaterialTheme.typography.titleSmall, + ) + }, + ) + } +} + +@Preview +@Composable +private fun LoanAccountWithdrawPreview() { + MifosMobileTheme { + LoanAccountWithdrawScreen( + state = LoanAccountWithdrawState(dialogState = null), + modifier = Modifier, + onAction = {}, + ) + } +} diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt new file mode 100644 index 000000000..90cb538bc --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanAccountWithdraw + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.error_loan_account_withdraw +import mifos_mobile.feature.loan.generated.resources.loan_application_withdrawn_successfully +import org.jetbrains.compose.resources.getString +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.common.DateHelper +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithdraw +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanAccountWithdrawViewModel( + private val loanRepositoryImp: LoanRepository, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = LoanAccountWithdrawState( + dialogState = null, + loanId = savedStateHandle.getStateFlow( + key = Constants.LOAN_ID, + initialValue = null, + ).value, + ), +) { + + init { + loanWithAssociations() + } + + private fun updateState(update: (LoanAccountWithdrawState) -> LoanAccountWithdrawState) { + mutableStateFlow.update(update) + } + + fun loanWithAssociations() { + updateState { it.copy(dialogState = LoanAccountWithdrawState.DialogState.Loading) } + viewModelScope.launch { + loanRepositoryImp.getLoanWithAssociations(Constants.TRANSACTIONS, state.loanId) + .catch { error -> + updateState { + it.copy(dialogState = LoanAccountWithdrawState.DialogState.Error(error.message.toString())) + } + }.collect { loanAssociations -> + updateState { + it.copy( + dialogState = null, + loanWithAssociations = loanAssociations.data, + ) + } + } + } + } + + private fun withdrawLoanAccount() { + val loanWithdraw = LoanWithdraw( + withdrawnOnDate = DateHelper.getDateAsStringFromLong(Clock.System.now().toEpochMilliseconds()), + note = state.loanReason, + ) + updateState { + it.copy(dialogState = LoanAccountWithdrawState.DialogState.Loading) + } + + viewModelScope.launch { + val errorMessage = getString(Res.string.error_loan_account_withdraw) + val successMessage = getString(Res.string.loan_application_withdrawn_successfully) + val response = loanRepositoryImp.withdrawLoanAccount( + loanId = state.loanId, + loanWithdraw = loanWithdraw, + ) + when (response) { + is DataState.Loading -> { + updateState { + it.copy(dialogState = LoanAccountWithdrawState.DialogState.Loading) + } + } + is DataState.Success -> { + updateState { + it.copy(dialogState = null) + } + sendEvent(LoanAccountWithdrawEvent.ShowToast(successMessage)) + sendEvent(LoanAccountWithdrawEvent.NavigateBack) + } + is DataState.Error -> { + updateState { + it.copy(dialogState = LoanAccountWithdrawState.DialogState.Error(errorMessage)) + } + } + } + } + } + + override fun handleAction(action: LoanAccountWithdrawAction) { + when (action) { + LoanAccountWithdrawAction.BackPress -> sendEvent(LoanAccountWithdrawEvent.NavigateBack) + is LoanAccountWithdrawAction.LoanReasonChanged -> { + updateState { + it.copy(loanReason = action.loanReason) + } + } + is LoanAccountWithdrawAction.WithDrawClicked -> { + withdrawLoanAccount() + } + } + } +} + +@Parcelize +data class LoanAccountWithdrawState( + val loanId: Long? = null, + val loanReason: String = "", + val reason: String = "", + val dialogState: DialogState?, + val loanWithAssociations: LoanWithAssociations? = null, +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface LoanAccountWithdrawEvent { + data object NavigateBack : LoanAccountWithdrawEvent + data class ShowToast(val message: String) : LoanAccountWithdrawEvent +} + +sealed interface LoanAccountWithdrawAction { + data class LoanReasonChanged(val loanReason: String) : LoanAccountWithdrawAction + data object BackPress : LoanAccountWithdrawAction + data object WithDrawClicked : LoanAccountWithdrawAction +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt similarity index 55% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt index 55ed6382c..7fb42dce2 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt @@ -9,7 +9,6 @@ */ package org.mifos.mobile.feature.loan.loanRepaymentSchedule -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -22,99 +21,118 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.designsystem.components.MifosScaffold +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.account_number +import mifos_mobile.feature.loan.generated.resources.date +import mifos_mobile.feature.loan.generated.resources.disbursement_date +import mifos_mobile.feature.loan.generated.resources.loan_balance +import mifos_mobile.feature.loan.generated.resources.loan_repayment_schedule +import mifos_mobile.feature.loan.generated.resources.no_of_payments +import mifos_mobile.feature.loan.generated.resources.repayment +import mifos_mobile.feature.loan.generated.resources.repayment_schedule +import mifos_mobile.feature.loan.generated.resources.s_no +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.common.DateHelper +import org.mifos.mobile.core.designsystem.component.MifosCard +import org.mifos.mobile.core.designsystem.component.MifosScaffold +import org.mifos.mobile.core.designsystem.icon.MifosIcons import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations import org.mifos.mobile.core.model.entity.accounts.loan.Periods import org.mifos.mobile.core.ui.component.EmptyDataView import org.mifos.mobile.core.ui.component.MifosErrorComponent import org.mifos.mobile.core.ui.component.MifosProgressIndicator -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R +import org.mifos.mobile.core.ui.utils.EventsEffect @Composable internal fun LoanRepaymentScheduleScreen( navigateBack: () -> Unit, modifier: Modifier = Modifier, - viewmodel: LoanRepaymentScheduleViewModel = hiltViewModel(), + viewModel: LoanRepaymentScheduleViewModel = koinViewModel(), ) { - val loanRepaymentScheduleUiState by viewmodel.loanUiState.collectAsStateWithLifecycle() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + LoanRepaymentScheduleEvent.NavigateBack -> navigateBack.invoke() + } + } + + LoanRepaymentScheduleDialog( + dialogState = state.dialogState, + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + ) LoanRepaymentScheduleScreen( - loanRepaymentScheduleUiState = loanRepaymentScheduleUiState, - navigateBack = navigateBack, - onRetry = viewmodel::loanLoanWithAssociations, + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, modifier = modifier, ) } +@Composable +private fun LoanRepaymentScheduleDialog( + dialogState: LoanRepaymentScheduleState.DialogState?, + state: LoanRepaymentScheduleState, + onAction: (LoanRepaymentScheduleAction) -> Unit, +) { + when (dialogState) { + is LoanRepaymentScheduleState.DialogState.Loading -> { + MifosProgressIndicator( + modifier = Modifier + .fillMaxSize(), + ) + } + + is LoanRepaymentScheduleState.DialogState.Error -> { + MifosErrorComponent( + isNetworkConnected = state.isOnline, + isRetryEnabled = true, + onRetry = { onAction(LoanRepaymentScheduleAction.RetryClicked) }, + ) + } + null -> Unit + } +} + @Composable private fun LoanRepaymentScheduleScreen( - loanRepaymentScheduleUiState: LoanUiState, - navigateBack: () -> Unit, - onRetry: () -> Unit, + state: LoanRepaymentScheduleState, + onAction: (LoanRepaymentScheduleAction) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - MifosScaffold( - topBarTitleResId = R.string.loan_repayment_schedule, - navigateBack = navigateBack, + topBarTitle = stringResource(Res.string.loan_repayment_schedule), + backPress = { (onAction(LoanRepaymentScheduleAction.BackPress)) }, modifier = modifier, ) { contentPadding -> - when (loanRepaymentScheduleUiState) { - LoanUiState.Loading -> { - MifosProgressIndicator( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), - ) - } - - is LoanUiState.ShowError -> { - MifosErrorComponent( - isNetworkConnected = Network.isConnected(context), - isRetryEnabled = true, - onRetry = onRetry, - ) - } - - is LoanUiState.ShowLoan -> { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(contentPadding), - ) { - LoanRepaymentScheduleCard(loanRepaymentScheduleUiState.loanWithAssociations) - RepaymentScheduleTable( - periods = loanRepaymentScheduleUiState.loanWithAssociations.repaymentSchedule?.periods!!, - currency = loanRepaymentScheduleUiState.loanWithAssociations.currency?.displaySymbol - ?: "$", - ) - } - } - - else -> {} + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding), + ) { + state.loanWithAssociations?.let { LoanRepaymentScheduleCard(it) } + RepaymentScheduleTable( + periods = state.loanWithAssociations?.repaymentSchedule?.periods!!, + currency = state.loanWithAssociations.currency?.displaySymbol ?: "$", + ) } } } @@ -124,26 +142,28 @@ private fun LoanRepaymentScheduleCard( loanWithAssociations: LoanWithAssociations, modifier: Modifier = Modifier, ) { - Card( + MifosCard( modifier = modifier .fillMaxWidth() .padding(8.dp) .border(1.dp, Color.Gray, RoundedCornerShape(12.dp)), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background), ) { Column( modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { LoanRepaymentScheduleCardItem( - label = stringResource(id = R.string.account_number), + label = stringResource(Res.string.account_number), value = loanWithAssociations.accountNo ?: "--", ) LoanRepaymentScheduleCardItem( - label = stringResource(id = R.string.disbursement_date), - value = DateHelper.getDateAsString(loanWithAssociations.timeline?.expectedDisbursementDate), + label = stringResource(Res.string.disbursement_date), + value = DateHelper.getDateAsString( + loanWithAssociations.timeline?.expectedDisbursementDate ?: emptyList(), + ), ) LoanRepaymentScheduleCardItem( - label = stringResource(id = R.string.no_of_payments), + label = stringResource(Res.string.no_of_payments), value = loanWithAssociations.numberOfRepayments.toString(), ) } @@ -164,10 +184,10 @@ private fun RepaymentScheduleTable( ) { item { Row { - TableCell(text = stringResource(id = R.string.s_no), weight = 0.5f) - TableCell(text = stringResource(id = R.string.date), weight = 1f) - TableCell(text = stringResource(id = R.string.loan_balance), weight = 1f) - TableCell(text = stringResource(id = R.string.repayment), weight = 1f) + TableCell(text = stringResource(Res.string.s_no), weight = 0.5f) + TableCell(text = stringResource(Res.string.date), weight = 1f) + TableCell(text = stringResource(Res.string.loan_balance), weight = 1f) + TableCell(text = stringResource(Res.string.repayment), weight = 1f) } } items(periods) { period -> @@ -190,7 +210,7 @@ private fun RepaymentScheduleTable( } } } else { - EmptyDataView(icon = R.drawable.ic_charges, error = R.string.repayment_schedule) + EmptyDataView(icon = MifosIcons.Error, error = Res.string.repayment_schedule) } } @@ -238,26 +258,13 @@ private fun LoanRepaymentScheduleCardItem( } } -internal class LoanRepaymentSchedulePreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - LoanUiState.Loading, - LoanUiState.ShowError(R.string.repayment_schedule), - LoanUiState.ShowLoan(LoanWithAssociations()), - ) -} - -@DevicePreviews +@Preview @Composable -private fun LoanRepaymentScheduleScreenPreview( - @PreviewParameter(LoanRepaymentSchedulePreviewProvider::class) - loanUiState: LoanUiState, -) { +private fun LoanRepaymentScheduleScreenPreview() { MifosMobileTheme { LoanRepaymentScheduleScreen( - loanRepaymentScheduleUiState = loanUiState, - navigateBack = {}, - onRetry = {}, + state = LoanRepaymentScheduleState(dialogState = null), + onAction = {}, ) } } diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt new file mode 100644 index 000000000..ce18456b1 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanRepaymentSchedule + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.repayment_schedule +import org.jetbrains.compose.resources.getString +import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.data.repository.LoanRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations +import org.mifos.mobile.core.ui.utils.BaseViewModel + +internal class LoanRepaymentScheduleViewModel( + private val loanRepositoryImp: LoanRepository, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = LoanRepaymentScheduleState( + dialogState = null, + loanId = savedStateHandle.getStateFlow( + Constants.LOAN_ID, + initialValue = null, + ).value, + ), +) { + init { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + updateState { it.copy(isOnline = isConnected) } + } + } + fetchLoanWithAssociations() + } + + private fun updateState(update: (LoanRepaymentScheduleState) -> LoanRepaymentScheduleState) { + mutableStateFlow.update(update) + } + + private fun fetchLoanWithAssociations() { + updateState { it.copy(dialogState = LoanRepaymentScheduleState.DialogState.Loading) } + + viewModelScope.launch { + val errorMessage = getString(Res.string.repayment_schedule) + state.loanId?.let { loanId -> + loanRepositoryImp.getLoanWithAssociations(Constants.REPAYMENT_SCHEDULE, loanId) + .catch { + updateState { + it.copy( + dialogState = LoanRepaymentScheduleState.DialogState.Error(errorMessage), + ) + } + } + .collect { loanData -> + updateState { + it.copy( + loanWithAssociations = loanData.data, + dialogState = null, + ) + } + } + } ?: updateState { it.copy(dialogState = null) } + } + } + + override fun handleAction(action: LoanRepaymentScheduleAction) { + when (action) { + LoanRepaymentScheduleAction.RetryClicked -> { + fetchLoanWithAssociations() + } + LoanRepaymentScheduleAction.BackPress -> sendEvent(LoanRepaymentScheduleEvent.NavigateBack) + } + } +} + +@Parcelize +data class LoanRepaymentScheduleState( + val loanId: Long? = null, + val loanWithAssociations: LoanWithAssociations? = null, + val dialogState: DialogState?, + val isOnline: Boolean = false, +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface LoanRepaymentScheduleEvent { + data object NavigateBack : LoanRepaymentScheduleEvent +} + +sealed interface LoanRepaymentScheduleAction { + data object RetryClicked : LoanRepaymentScheduleAction + data object BackPress : LoanRepaymentScheduleAction +} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt similarity index 66% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt index efa4b49ce..84f4dc3e4 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationContent.kt @@ -9,24 +9,31 @@ */ package org.mifos.mobile.feature.loan.loanReview +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.mifos.mobile.core.designsystem.components.MifosButton +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.currency +import mifos_mobile.feature.loan.generated.resources.expected_disbursement_date +import mifos_mobile.feature.loan.generated.resources.loan_purpose +import mifos_mobile.feature.loan.generated.resources.principal +import mifos_mobile.feature.loan.generated.resources.product +import mifos_mobile.feature.loan.generated.resources.submission_date +import mifos_mobile.feature.loan.generated.resources.submit_loan +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.common.formatAmount +import org.mifos.mobile.core.designsystem.component.MifosButton import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme import org.mifos.mobile.core.ui.component.MifosTextTitleDescDoubleLine import org.mifos.mobile.core.ui.component.MifosTextTitleDescSingleLine -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R -import java.util.Locale @Composable internal fun ReviewLoanApplicationContent( @@ -34,81 +41,65 @@ internal fun ReviewLoanApplicationContent( onSubmit: () -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxSize()) { + Column( + modifier = modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement + .spacedBy(16.dp), + ) { Text( text = data.loanName ?: "", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) - Spacer(modifier = Modifier.height(8.dp)) - Text( text = data.accountNo ?: "", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.product), + title = stringResource(Res.string.product), description = data.loanProduct ?: "", descriptionStyle = MaterialTheme.typography.bodyMedium, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.loan_purpose), + title = stringResource(Res.string.loan_purpose), description = data.loanPurpose ?: "", descriptionStyle = MaterialTheme.typography.bodyMedium, ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescDoubleLine( - title = stringResource(id = R.string.principal), - description = String.format( - Locale.getDefault(), - "%.2f", - data.principal ?: 0.0, - ), + title = stringResource(Res.string.principal), + description = formatAmount(data.principal ?: 0.0), descriptionStyle = MaterialTheme.typography.bodyMedium, ) - Spacer(modifier = Modifier.height(16.dp)) - MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.currency), + title = stringResource(Res.string.currency), description = data.currency ?: "", ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.submission_date), + title = stringResource(Res.string.submission_date), description = data.submissionDate ?: "", ) - Spacer(modifier = Modifier.height(8.dp)) - MifosTextTitleDescSingleLine( - title = stringResource(id = R.string.expected_disbursement_date), + title = stringResource(Res.string.expected_disbursement_date), description = data.disbursementDate ?: "", ) - Spacer(modifier = Modifier.height(26.dp)) - MifosButton( - textResId = R.string.submit_loan, + text = { stringResource(Res.string.submit_loan) }, onClick = onSubmit, modifier = Modifier.fillMaxWidth(), ) } } -@DevicePreviews +@Preview @Composable private fun ReviewLoanApplicationContentPreview( modifier: Modifier = Modifier, diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt new file mode 100644 index 000000000..6d93bd816 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanReview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import mifos_mobile.feature.loan.generated.resources.Res +import mifos_mobile.feature.loan.generated.resources.no_internet_connection +import mifos_mobile.feature.loan.generated.resources.update_loan +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.mobile.core.designsystem.component.MifosTopBar +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme +import org.mifos.mobile.core.ui.component.MifosErrorComponent +import org.mifos.mobile.core.ui.component.MifosProgressIndicator +import org.mifos.mobile.core.ui.component.NoInternet +import org.mifos.mobile.core.ui.utils.EventsEffect + +@Composable +internal fun ReviewLoanApplicationScreen( + navigateBack: (isSuccess: Boolean) -> Unit, + modifier: Modifier = Modifier, + viewModel: ReviewLoanApplicationViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() + + val snackbarHostState = remember { SnackbarHostState() } + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + is ReviewLoanApplicationEvent.ShowToast -> { + scope.launch { + snackbarHostState.showSnackbar(event.message) + } + } + + is ReviewLoanApplicationEvent.NavigateBack -> { + navigateBack(event.isSuccess) + } + } + } + + LoanReviewDialogs( + dialogState = state.dialogState, + state = state, + ) + + ReviewLoanApplicationScreen( + state = state, + onAction = remember(viewModel) { + { viewModel.trySendAction(it) } + }, + modifier = modifier, + ) +} + +@Composable +private fun LoanReviewDialogs( + state: ReviewLoanApplicationState, + dialogState: ReviewLoanApplicationState.DialogState?, +) { + when (dialogState) { + is ReviewLoanApplicationState.DialogState.Error -> + MifosErrorComponent(isNetworkConnected = state.isOnline) + + ReviewLoanApplicationState.DialogState.Loading -> MifosProgressIndicator( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background.copy(0.8f)), + ) + + null -> Unit + } +} + +@Composable +private fun ReviewLoanApplicationScreen( + state: ReviewLoanApplicationState, + onAction: (ReviewLoanApplicationAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxSize()) { + MifosTopBar( + modifier = Modifier.fillMaxWidth(), + backPress = { onAction(ReviewLoanApplicationAction.NavigateBack(false)) }, + topBarTitle = stringResource(Res.string.update_loan), + ) + Box(modifier = Modifier.weight(1f)) { + ReviewLoanApplicationContent( + data = state.reviewLoanApplicationUiData, + onSubmit = { onAction(ReviewLoanApplicationAction.SubmitLoan) }, + modifier = Modifier.padding(16.dp), + ) + } + + if (!state.isOnline) { + NoInternet( + error = Res.string.no_internet_connection, + isRetryEnabled = false, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Preview +@Composable +private fun ReviewLoanApplicationScreenPreview() { + MifosMobileTheme { + ReviewLoanApplicationScreen( + state = ReviewLoanApplicationState(dialogState = null), + onAction = {}, + modifier = Modifier, + + ) + } +} diff --git a/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt new file mode 100644 index 000000000..7fb190a10 --- /dev/null +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.feature.loan.loanReview + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.mifos.mobile.core.common.DataState +import org.mifos.mobile.core.data.repository.ReviewLoanApplicationRepository +import org.mifos.mobile.core.data.util.NetworkMonitor +import org.mifos.mobile.core.model.Parcelable +import org.mifos.mobile.core.model.Parcelize +import org.mifos.mobile.core.model.entity.payload.LoansPayload +import org.mifos.mobile.core.model.enums.LoanState +import org.mifos.mobile.core.ui.utils.BaseViewModel +import org.mifos.mobile.feature.loan.navigation.LoanReviewArgs +import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_REVIEW_ARGS + +internal class ReviewLoanApplicationViewModel( + private val reviewLoanApplicationRepository: ReviewLoanApplicationRepository, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = ReviewLoanApplicationState( + dialogState = null, + ), +) { + + private val loanReviewArgsJson = savedStateHandle.getStateFlow(LOAN_REVIEW_ARGS, null) + + private val loanReviewArgs: StateFlow = loanReviewArgsJson.map { jsonString -> + jsonString?.let { Json.decodeFromString(it) } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) + + init { + observeNetworkStatus() + collectReviewLoanApplicationUiData() + } + + private fun updateState(update: (ReviewLoanApplicationState) -> ReviewLoanApplicationState) { + mutableStateFlow.update(update) + } + + private fun observeNetworkStatus() { + viewModelScope.launch { + networkMonitor.isOnline.collectLatest { connected -> + updateState { it.copy(isOnline = connected) } + } + } + } + + private fun collectReviewLoanApplicationUiData() { + viewModelScope.launch { + loanReviewArgs.collectLatest { args -> + args?.let { loanArgs -> + updateState { currentState -> + currentState.copy( + reviewLoanApplicationUiData = ReviewLoanApplicationUiData( + loanState = loanArgs.loanState, + loanName = loanArgs.loanName, + accountNo = loanArgs.accountNo, + loanProduct = loanArgs.loansPayload?.productName, + loanPurpose = loanArgs.loansPayload?.loanPurpose, + principal = loanArgs.loansPayload?.principal, + currency = loanArgs.loansPayload?.currency, + submissionDate = loanArgs.loansPayload?.submittedOnDate, + disbursementDate = loanArgs.loansPayload?.expectedDisbursementDate, + loanId = loanArgs.loanId ?: 0, + ), + ) + } + } + } + } + } + + override fun handleAction(action: ReviewLoanApplicationAction) { + when (action) { + is ReviewLoanApplicationAction.SubmitLoan -> submitLoan() + is ReviewLoanApplicationAction.NavigateBack -> + sendEvent(ReviewLoanApplicationEvent.NavigateBack(action.isSuccess)) + } + } + + private fun submitLoan() { + viewModelScope.launch { + updateState { it.copy(dialogState = ReviewLoanApplicationState.DialogState.Loading) } + try { + val result = reviewLoanApplicationRepository.submitLoan( + loanState = state.reviewLoanApplicationUiData.loanState, + loansPayload = loanReviewArgs.value?.loansPayload ?: LoansPayload(), + loanId = state.reviewLoanApplicationUiData.loanId, + ) + when (result) { + DataState.Loading -> updateState { + it.copy( + dialogState = + ReviewLoanApplicationState.DialogState.Loading, + ) + } + is DataState.Success -> { + sendEvent( + ReviewLoanApplicationEvent.ShowToast(result.data), + ) + sendEvent(ReviewLoanApplicationEvent.NavigateBack(true)) + } + is DataState.Error -> { + updateState { + it.copy( + dialogState = ReviewLoanApplicationState + .DialogState.Error(result.message), + ) + } + } + } + } catch (error: Exception) { + updateState { + it.copy( + dialogState = ReviewLoanApplicationState + .DialogState.Error(error.message ?: "An error occurred"), + ) + } + updateState { it.copy(dialogState = null) } + } + } + } +} + +@Parcelize +data class ReviewLoanApplicationState( + val isOnline: Boolean = false, + val dialogState: DialogState?, + val reviewLoanApplicationUiData: ReviewLoanApplicationUiData = ReviewLoanApplicationUiData(), +) : Parcelable { + sealed interface DialogState : Parcelable { + @Parcelize + data object Loading : DialogState + + @Parcelize + data class Error(val message: String) : DialogState + } +} + +sealed interface ReviewLoanApplicationAction { + data object SubmitLoan : ReviewLoanApplicationAction + data class NavigateBack(val isSuccess: Boolean) : ReviewLoanApplicationAction +} + +sealed interface ReviewLoanApplicationEvent { + data class NavigateBack(val isSuccess: Boolean) : ReviewLoanApplicationEvent + data class ShowToast(val message: String) : ReviewLoanApplicationEvent +} + +@Parcelize +data class ReviewLoanApplicationUiData( + val loanId: Long = 0, + val loanState: LoanState = LoanState.CREATE, + val accountNo: String? = null, + val loanName: String? = null, + val disbursementDate: String? = null, + val submissionDate: String? = null, + val currency: String? = null, + val principal: Double? = null, + val loanPurpose: String? = null, + val loanProduct: String? = null, +) : Parcelable diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt similarity index 75% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt index 276e75ed1..52239de82 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavGraph.kt @@ -15,7 +15,11 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.navArgument +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.mifos.mobile.core.common.Constants +import org.mifos.mobile.core.model.entity.payload.LoansPayload import org.mifos.mobile.core.model.enums.ChargeType import org.mifos.mobile.core.model.enums.LoanState import org.mifos.mobile.feature.loan.loanAccount.LoanAccountDetailScreen @@ -39,22 +43,8 @@ fun NavController.navigateToLoanApplication() { ) } -fun NavController.navigateToLoanReview( - loanState: LoanState, - loansPayloadString: String, - loanId: Long?, - loanName: String, - accountNo: String, -) { - navigate( - LoanNavigation.LoanReview.passArguments( - accountNo = accountNo, - loanId = loanId, - loanState = loanState, - loansPayload = loansPayloadString, - loanName = loanName, - ), - ) +fun NavController.navigateToLoanReview(args: LoanReviewArgs) { + navigate(LoanNavigation.LoanReview.passArguments(args)) } fun NavGraphBuilder.loanNavGraph( @@ -102,8 +92,28 @@ fun NavGraphBuilder.loanNavGraph( loanApplication( navigateBack = navController::popBackStack, - reviewNewLoanApplication = navController::navigateToLoanReview, - submitUpdateLoanApplication = navController::navigateToLoanReview, + reviewNewLoanApplication = { loanState, loansPayload, loanId, loanName, accountNo -> + navController.navigateToLoanReview( + LoanReviewArgs( + loanState = loanState, + loanId = loanId, + loanName = loanName, + accountNo = accountNo, + loansPayloadJson = Json.encodeToString(loansPayload), + ), + ) + }, + submitUpdateLoanApplication = { loanState, loansPayload, loanId, loanName, accountNo -> + navController.navigateToLoanReview( + LoanReviewArgs( + loanState = loanState, + loanId = loanId, + loanName = loanName, + accountNo = accountNo, + loansPayloadJson = Json.encodeToString(loansPayload), + ), + ) + }, ) loanSummary( @@ -180,7 +190,9 @@ fun NavGraphBuilder.loanApplication( route = LoanNavigation.LoanApplication.route, arguments = listOf( navArgument(Constants.LOAN_ID) { type = NavType.LongType }, - navArgument(Constants.LOAN_STATE) { type = NavType.EnumType(LoanState::class.java) }, +// navArgument(Constants.LOAN_STATE) { type = NavType.EnumType(LoanState::class.java) }, + navArgument(Constants.LOAN_STATE) { type = NavType.StringType }, + ), ) { LoanApplicationScreen( @@ -248,16 +260,33 @@ fun NavGraphBuilder.loanReview( ) { composable( route = LoanNavigation.LoanReview.route, - arguments = listOf( - navArgument(Constants.LOAN_ID) { type = NavType.LongType }, - navArgument(Constants.LOANS_PAYLOAD) { type = NavType.StringType }, - navArgument(Constants.LOAN_NAME) { type = NavType.StringType }, - navArgument(Constants.ACCOUNT_NUMBER) { type = NavType.StringType }, - navArgument(Constants.LOAN_STATE) { type = NavType.EnumType(LoanState::class.java) }, - ), - ) { - ReviewLoanApplicationScreen( - navigateBack = { navigateBack() }, - ) + arguments = listOf(navArgument(LoanRoute.LOAN_REVIEW_ARGS) { type = NavType.StringType }), + ) { backStackEntry -> + val jsonArgs = backStackEntry.arguments?.getString(LoanRoute.LOAN_REVIEW_ARGS) + val loanReviewArgs = jsonArgs?.let { LoanReviewArgs.fromJson(it) } + + loanReviewArgs?.let { + ReviewLoanApplicationScreen( + navigateBack = { navigateBack() }, + ) + } + } +} + +@Serializable +data class LoanReviewArgs( + val loanState: LoanState, + val loanId: Long?, + val loanName: String, + val accountNo: String, + val loansPayloadJson: String?, +) { + val loansPayload: LoansPayload? + get() = loansPayloadJson?.let { Json.decodeFromString(it) } + + fun toJson(): String = Json.encodeToString(this) + + companion object { + fun fromJson(json: String): LoanReviewArgs = Json.decodeFromString(json) } } diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt similarity index 82% rename from feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt rename to feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt index d92b7257b..a18659c72 100644 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt +++ b/feature/loan/src/commonMain/kotlin/org/mifos/mobile/feature/loan/navigation/LoanNavigation.kt @@ -9,15 +9,13 @@ */ package org.mifos.mobile.feature.loan.navigation -import org.mifos.mobile.core.common.Constants.ACCOUNT_NUMBER -import org.mifos.mobile.core.common.Constants.LOANS_PAYLOAD import org.mifos.mobile.core.common.Constants.LOAN_ID -import org.mifos.mobile.core.common.Constants.LOAN_NAME import org.mifos.mobile.core.common.Constants.LOAN_STATE import org.mifos.mobile.core.model.enums.LoanState import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_APPLICATION_SCREEN_ROUTE import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_DETAIL_SCREEN_ROUTE import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_NAVIGATION_ROUTE_BASE +import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_REVIEW_ARGS import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_REVIEW_SCREEN_ROUTE import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_SCHEDULE_SCREEN_ROUTE import org.mifos.mobile.feature.loan.navigation.LoanRoute.LOAN_SUMMARY_SCREEN_ROUTE @@ -54,17 +52,9 @@ sealed class LoanNavigation(val route: String) { fun passArguments(loanId: Long) = "$LOAN_SCHEDULE_SCREEN_ROUTE/$loanId" } - data object LoanReview : LoanNavigation( - route = "$LOAN_REVIEW_SCREEN_ROUTE/{$LOAN_STATE}/{${LOANS_PAYLOAD}}/{$LOAN_ID}/{$LOAN_NAME}/{$ACCOUNT_NUMBER}", - ) { - fun passArguments( - loanState: LoanState, - loansPayload: String, - loanId: Long? = null, - loanName: String, - accountNo: String, - ): String { - return "$LOAN_REVIEW_SCREEN_ROUTE/$loanState/$loansPayload/$loanId/$loanName/$accountNo" + data object LoanReview : LoanNavigation(route = "$LOAN_REVIEW_SCREEN_ROUTE/{$LOAN_REVIEW_ARGS}") { + fun passArguments(args: LoanReviewArgs): String { + return "$LOAN_REVIEW_SCREEN_ROUTE/${args.toJson()}" } } } @@ -78,4 +68,6 @@ object LoanRoute { const val LOAN_WITHDRAW_SCREEN_ROUTE = "loan_withdraw_screen_route" const val LOAN_SCHEDULE_SCREEN_ROUTE = "loan_schedule_screen_route" const val LOAN_REVIEW_SCREEN_ROUTE = "loan_review_screen_route" + + const val LOAN_REVIEW_ARGS = "loanReviewArgs" } diff --git a/feature/loan/src/main/AndroidManifest.xml b/feature/loan/src/main/AndroidManifest.xml deleted file mode 100644 index 1f9b243f0..000000000 --- a/feature/loan/src/main/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt deleted file mode 100644 index 01e8a91bf..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountDetailScreen.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccount - -import android.widget.Toast -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.common.Constants.TRANSFER_PAY_TO -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.designsystem.components.MifosScaffold -import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.ui.component.EmptyDataView -import org.mifos.mobile.core.ui.component.MifosProgressIndicator -import org.mifos.mobile.core.ui.component.NoInternet -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R - -@Composable -internal fun LoanAccountDetailScreen( - navigateBack: () -> Unit, - viewGuarantor: (loanId: Long) -> Unit, - updateLoan: (Long) -> Unit, - withdrawLoan: (Long) -> Unit, - viewLoanSummary: (Long) -> Unit, - viewCharges: () -> Unit, - viewRepaymentSchedule: (Long) -> Unit, - viewTransactions: (Long) -> Unit, - viewQr: (String) -> Unit, - makePayment: (accountId: Long, outstandingBalance: Double?, transferType: String) -> Unit, - modifier: Modifier = Modifier, - viewModel: LoanAccountsDetailViewModel = hiltViewModel(), -) { - val uiState by viewModel.loanUiState.collectAsStateWithLifecycle() - val loanId by viewModel.loanId.collectAsStateWithLifecycle() - - LoanAccountDetailScreen( - uiState = uiState, - navigateBack = navigateBack, - viewGuarantor = { viewGuarantor(loanId) }, - updateLoan = { updateLoan(loanId) }, - withdrawLoan = { withdrawLoan(loanId) }, - retryConnection = viewModel::loadLoanAccountDetails, - viewLoanSummary = { viewLoanSummary(loanId) }, - viewCharges = viewCharges, - modifier = modifier, - viewRepaymentSchedule = { viewRepaymentSchedule(loanId) }, - viewTransactions = { viewTransactions(loanId) }, - viewQr = { viewQr(viewModel.getQrString()) }, - makePayment = { - makePayment( - loanId, - viewModel.loanWithAssociations?.summary?.totalOutstanding, - TRANSFER_PAY_TO, - ) - }, - ) -} - -@Composable -private fun LoanAccountDetailScreen( - uiState: LoanAccountDetailUiState, - navigateBack: () -> Unit, - viewGuarantor: () -> Unit, - updateLoan: () -> Unit, - withdrawLoan: () -> Unit, - retryConnection: () -> Unit, - viewLoanSummary: () -> Unit, - viewCharges: () -> Unit, - viewRepaymentSchedule: () -> Unit, - viewTransactions: () -> Unit, - viewQr: () -> Unit, - makePayment: () -> Unit, - modifier: Modifier = Modifier, -) { - MifosScaffold( - modifier = modifier, - topBar = { - LoanAccountDetailTopBar( - navigateBack = navigateBack, - viewGuarantor = viewGuarantor, - updateLoan = updateLoan, - withdrawLoan = withdrawLoan, - ) - }, - content = { - Box(modifier = Modifier.padding(it)) { - when (uiState) { - is LoanAccountDetailUiState.Success -> { - LoanAccountDetailContent( - loanWithAssociations = uiState.loanWithAssociations, - viewLoanSummary = viewLoanSummary, - viewCharges = viewCharges, - viewRepaymentSchedule = viewRepaymentSchedule, - viewTransactions = viewTransactions, - viewQr = viewQr, - makePayment = makePayment, - ) - } - - is LoanAccountDetailUiState.Loading -> { - MifosProgressIndicator(modifier = Modifier.fillMaxSize()) - } - - is LoanAccountDetailUiState.Error -> { - ErrorComponent(retryConnection = retryConnection) - } - - is LoanAccountDetailUiState.ApprovalPending -> { - EmptyDataView( - modifier = Modifier.fillMaxSize(), - icon = R.drawable.ic_assignment_turned_in_black_24dp, - error = R.string.approval_pending, - ) - } - - is LoanAccountDetailUiState.WaitingForDisburse -> { - EmptyDataView( - modifier = Modifier.fillMaxSize(), - icon = R.drawable.ic_assignment_turned_in_black_24dp, - error = R.string.waiting_for_disburse, - ) - } - } - } - }, - ) -} - -@Composable -private fun ErrorComponent( - retryConnection: () -> Unit, -) { - val context = LocalContext.current - if (!Network.isConnected(context)) { - NoInternet( - error = R.string.no_internet_connection, - isRetryEnabled = true, - retry = retryConnection, - ) - Toast.makeText( - context, - stringResource(R.string.internet_not_connected), - Toast.LENGTH_SHORT, - ).show() - } else { - EmptyDataView( - icon = R.drawable.ic_error_black_24dp, - error = R.string.loan_account_details, - modifier = Modifier.fillMaxSize(), - ) - } -} - -@Composable -@DevicePreviews -private fun LoanAccountDetailScreenPreview() { - MifosMobileTheme { - LoanAccountDetailScreen( - uiState = LoanAccountDetailUiState.Success(LoanWithAssociations()), - {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, - ) - } -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt deleted file mode 100644 index c1389997b..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccount/LoanAccountsDetailViewModel.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccount - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.launch -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.common.Constants.LOAN_ID -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.datastore.PreferencesHelper -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.model.enums.AccountType -import org.mifos.mobile.core.qr.QrCodeGenerator -import javax.inject.Inject - -@HiltViewModel -internal class LoanAccountsDetailViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, - private val preferencesHelper: PreferencesHelper, -) : ViewModel() { - - private val _loanUiState = - MutableStateFlow(LoanAccountDetailUiState.Loading) - val loanUiState: StateFlow get() = _loanUiState - - val loanId = savedStateHandle.getStateFlow(key = LOAN_ID, initialValue = -1L) - - private var _loanWithAssociations: LoanWithAssociations? = null - val loanWithAssociations get() = _loanWithAssociations - - init { - loadLoanAccountDetails() - } - - fun loadLoanAccountDetails() { - viewModelScope.launch { - _loanUiState.value = LoanAccountDetailUiState.Loading - loanRepositoryImp.getLoanWithAssociations(Constants.REPAYMENT_SCHEDULE, loanId.value) - .catch { _loanUiState.value = LoanAccountDetailUiState.Error } - .collect { processLoanDetailsResponse(it) } - } - } - - private fun processLoanDetailsResponse(loanWithAssociations: LoanWithAssociations?) { - _loanWithAssociations = loanWithAssociations - val uiState = when { - loanWithAssociations == null -> LoanAccountDetailUiState.Error - loanWithAssociations.status?.active == true -> LoanAccountDetailUiState.Success( - loanWithAssociations, - ) - - loanWithAssociations.status?.pendingApproval == true -> LoanAccountDetailUiState.ApprovalPending - loanWithAssociations.status?.waitingForDisbursal == true -> LoanAccountDetailUiState.WaitingForDisburse - else -> LoanAccountDetailUiState.Success(loanWithAssociations) - } - _loanUiState.value = uiState - } - - fun getQrString(): String { - return QrCodeGenerator.getAccountDetailsInString( - loanWithAssociations?.accountNo, - preferencesHelper.officeName, - AccountType.LOAN, - ) - } -} - -internal sealed class LoanAccountDetailUiState { - data object Loading : LoanAccountDetailUiState() - data object Error : LoanAccountDetailUiState() - data object ApprovalPending : LoanAccountDetailUiState() - data object WaitingForDisburse : LoanAccountDetailUiState() - data class Success(val loanWithAssociations: LoanWithAssociations) : LoanAccountDetailUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt deleted file mode 100644 index 8d7c3000e..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationScreen.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountApplication - -import android.content.Context -import androidx.compose.foundation.layout.Box -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.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.gson.Gson -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.designsystem.components.MifosTopBar -import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme -import org.mifos.mobile.core.model.entity.payload.LoansPayload -import org.mifos.mobile.core.model.enums.LoanState -import org.mifos.mobile.core.ui.component.MifosErrorComponent -import org.mifos.mobile.core.ui.component.MifosProgressIndicatorOverlay -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R - -@Composable -internal fun LoanApplicationScreen( - navigateBack: () -> Unit, - reviewNewLoanApplication: ( - loanState: LoanState, - loansPayloadString: String, - loanId: Long?, - loanName: String, - accountNo: String, - ) -> Unit, - submitUpdateLoanApplication: ( - loanState: LoanState, - loansPayloadString: String, - loanId: Long?, - loanName: String, - accountNo: String, - ) -> Unit, - modifier: Modifier = Modifier, - viewModel: LoanApplicationViewModel = hiltViewModel(), -) { - val context = LocalContext.current - - val uiState by viewModel.loanUiState.collectAsStateWithLifecycle() - val uiData by viewModel.loanApplicationScreenData.collectAsStateWithLifecycle() - val loanState by viewModel.loanState.collectAsStateWithLifecycle() - - LaunchedEffect(key1 = loanState) { - viewModel.loadLoanApplicationTemplate(loanState) - } - - LoanApplicationScreen( - uiState = uiState, - uiData = uiData, - navigateBack = navigateBack, - loanState = loanState, - onRetry = { viewModel.loadLoanApplicationTemplate(loanState) }, - modifier = modifier, - selectProduct = viewModel::productSelected, - selectPurpose = viewModel::purposeSelected, - setDisbursementDate = viewModel::setDisburseDate, - reviewClicked = { - viewModel.setPrincipalAmount(it) - getLoanPayload( - context = context, - loanState = loanState, - reviewNewLoanApplication = reviewNewLoanApplication, - submitUpdateLoanApplication = submitUpdateLoanApplication, - viewModel = viewModel, - ) - }, - ) -} - -@Composable -private fun LoanApplicationScreen( - uiState: LoanApplicationUiState, - loanState: LoanState, - uiData: LoanApplicationScreenData, - navigateBack: () -> Unit, - selectProduct: (Int) -> Unit, - selectPurpose: (Int) -> Unit, - setDisbursementDate: (String) -> Unit, - reviewClicked: (String) -> Unit, - onRetry: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - Scaffold( - modifier = modifier, - topBar = { - MifosTopBar( - modifier = Modifier.fillMaxWidth(), - navigateBack = { navigateBack() }, - title = { - Text( - text = stringResource( - id = if (loanState == LoanState.CREATE) { - R.string.apply_for_loan - } else { - R.string.update_loan - }, - ), - ) - }, - ) - }, - content = { - Column( - modifier = Modifier - .padding(it) - .fillMaxSize(), - ) { - Box(modifier = Modifier.weight(1f)) { - LoanApplicationContent( - uiData = uiData, - selectProduct = selectProduct, - selectPurpose = selectPurpose, - reviewClicked = reviewClicked, - setDisbursementDate = setDisbursementDate, - ) - when (uiState) { - is LoanApplicationUiState.Success -> Unit - - is LoanApplicationUiState.Loading -> { - MifosProgressIndicatorOverlay() - } - - is LoanApplicationUiState.Error -> { - MifosErrorComponent( - isNetworkConnected = Network.isConnected(context), - isEmptyData = false, - isRetryEnabled = true, - onRetry = onRetry, - ) - } - } - } - } - }, - ) -} - -internal class UiStatesParameterProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - LoanApplicationUiState.Error(R.string.something_went_wrong), - LoanApplicationUiState.Loading, - LoanApplicationUiState.Success, - ) -} - -@DevicePreviews -@Composable -private fun ReviewLoanApplicationScreenPreview( - @PreviewParameter(UiStatesParameterProvider::class) - loanApplicationUiState: LoanApplicationUiState, -) { - MifosMobileTheme { - LoanApplicationScreen( - uiState = loanApplicationUiState, - uiData = LoanApplicationScreenData(), - loanState = LoanState.CREATE, - navigateBack = {}, - selectPurpose = {}, - selectProduct = {}, - reviewClicked = {}, - setDisbursementDate = {}, - onRetry = {}, - ) - } -} - -private fun getLoanPayload( - context: Context, - viewModel: LoanApplicationViewModel, - loanState: LoanState, - reviewNewLoanApplication: ( - loanState: LoanState, - loansPayloadString: String, - loanId: Long?, - loanName: String, - accountNo: String, - ) -> Unit, - submitUpdateLoanApplication: ( - loanState: LoanState, - loansPayloadString: String, - loanId: Long?, - loanName: String, - accountNo: String, - ) -> Unit, -) { - val payload = LoansPayload().apply { - clientId = viewModel.loanTemplate.clientId.takeIf { loanState == LoanState.CREATE } - loanPurpose = - viewModel.loanApplicationScreenData.value.selectedLoanPurpose ?: "Not provided" - productName = viewModel.loanApplicationScreenData.value.selectedLoanProduct - currency = viewModel.loanApplicationScreenData.value.currencyLabel - if (viewModel.purposeId > 0) loanPurposeId = viewModel.purposeId - productId = viewModel.productId - principal = - viewModel.loanApplicationScreenData.value.principalAmount?.toDoubleOrNull() ?: 0.0 - loanTermFrequency = viewModel.loanTemplate.termFrequency - loanTermFrequencyType = viewModel.loanTemplate.interestRateFrequencyType?.id - loanType = "individual".takeIf { loanState == LoanState.CREATE } - numberOfRepayments = viewModel.loanTemplate.numberOfRepayments - repaymentEvery = viewModel.loanTemplate.repaymentEvery - repaymentFrequencyType = viewModel.loanTemplate.interestRateFrequencyType?.id - interestRatePerPeriod = viewModel.loanTemplate.interestRatePerPeriod - expectedDisbursementDate = DateHelper.getSpecificFormat( - DateHelper.FORMAT_MMMM, - viewModel.loanApplicationScreenData.value.disbursementDate, - ) - submittedOnDate = DateHelper.getSpecificFormat( - DateHelper.FORMAT_MMMM, - viewModel.loanApplicationScreenData.value.submittedDate, - ).takeIf { loanState == LoanState.CREATE } - - transactionProcessingStrategyId = viewModel.loanTemplate.transactionProcessingStrategyId - amortizationType = viewModel.loanTemplate.amortizationType?.id - interestCalculationPeriodType = viewModel.loanTemplate.interestCalculationPeriodType?.id - interestType = viewModel.loanTemplate.interestType?.id - } - - val loansPayloadString = Gson().toJson(payload) - when (loanState) { - LoanState.CREATE -> reviewNewLoanApplication( - loanState, - loansPayloadString, - viewModel.loanId.value, - context.getString( - R.string.string_and_string, - context.getString(R.string.new_loan_application) + " ", - viewModel.loanApplicationScreenData.value.clientName ?: "", - ), - context.getString( - R.string.string_and_string, - context.getString(R.string.account_number) + " ", - viewModel.loanApplicationScreenData.value.accountNumber ?: "", - ), - ) - - LoanState.UPDATE -> submitUpdateLoanApplication( - loanState, - loansPayloadString, - null, - context.getString( - R.string.string_and_string, - context.getString(R.string.update_loan_application) + " ", - viewModel.loanApplicationScreenData.value.clientName ?: "", - ), - context.getString( - R.string.string_and_string, - context.getString(R.string.account_number) + " ", - viewModel.loanApplicationScreenData.value.accountNumber ?: "", - ), - ) - } -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt deleted file mode 100644 index 11936dd28..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountApplication/LoanApplicationViewModel.kt +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountApplication - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.common.utils.getTodayFormatted -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.model.entity.templates.loans.LoanTemplate -import org.mifos.mobile.core.model.enums.LoanState -import org.mifos.mobile.core.network.Result -import org.mifos.mobile.core.network.asResult -import org.mifos.mobile.feature.loan.R -import org.mifos.mobile.feature.loan.loanAccountApplication.LoanApplicationUiState.Loading -import java.time.Instant -import java.util.Locale -import javax.inject.Inject - -@HiltViewModel -internal class LoanApplicationViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - - var loanUiState: StateFlow = MutableStateFlow(Loading) - - val loanId = savedStateHandle.getStateFlow(key = Constants.LOAN_ID, initialValue = null) - val loanState = savedStateHandle.getStateFlow( - key = Constants.LOAN_STATE, - initialValue = LoanState.CREATE, - ) - - var loanWithAssociations: StateFlow = loanId - .flatMapLatest { - loanRepositoryImp.getLoanWithAssociations(Constants.TRANSACTIONS, it) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = null, - ) - - private val _loanApplicationScreenData = MutableStateFlow(LoanApplicationScreenData()) - val loanApplicationScreenData: StateFlow = _loanApplicationScreenData - - var loanTemplate: LoanTemplate = LoanTemplate() - var productId: Int = 0 - var purposeId: Int = 0 - private var isLoanUpdatePurposesInitialization: Boolean = true - - init { - _loanApplicationScreenData.update { - it.copy( - submittedDate = getTodayFormatted(), - disbursementDate = getTodayFormatted(), - ) - } - } - - fun loadLoanApplicationTemplate(loanState: LoanState) { - loanUiState = loanRepositoryImp.template() - .asResult() - .map { result -> - when (result) { - is Result.Success -> { - loanTemplate = result.data ?: LoanTemplate() - if (loanState == LoanState.CREATE) { - showLoanTemplate(loanTemplate = loanTemplate) - } else { - showUpdateLoanTemplate(loanTemplate = loanTemplate) - } - LoanApplicationUiState.Success - } - - is Result.Loading -> Loading - is Result.Error -> LoanApplicationUiState.Error(R.string.error_fetching_template) - } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = Loading, - ) - } - - private fun loadLoanApplicationTemplateByProduct(productId: Int?, loanState: LoanState) { - loanUiState = loanRepositoryImp.getLoanTemplateByProduct(productId) - .asResult() - .map { result -> - when (result) { - is Result.Success -> { - result.data?.let { - if (loanState == LoanState.CREATE) { - showLoanTemplateByProduct( - loanTemplate = it, - ) - } else { - showUpdateLoanTemplateByProduct(loanTemplate = it) - } - } - LoanApplicationUiState.Success - } - - is Result.Loading -> Loading - is Result.Error -> LoanApplicationUiState.Error(R.string.error_fetching_template) - } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = Loading, - ) - } - - private fun showLoanTemplate(loanTemplate: LoanTemplate) { - val listLoanProducts = refreshLoanProductList(loanTemplate = loanTemplate) - _loanApplicationScreenData.update { - it.copy(listLoanProducts = listLoanProducts) - } - } - - private fun showUpdateLoanTemplate(loanTemplate: LoanTemplate) { - val listLoanProducts = refreshLoanProductList(loanTemplate = loanTemplate) - _loanApplicationScreenData.update { - it.copy( - listLoanProducts = listLoanProducts, - selectedLoanProduct = loanWithAssociations.value?.loanProductName, - accountNumber = loanWithAssociations.value?.accountNo, - clientName = loanWithAssociations.value?.clientName, - currencyLabel = loanWithAssociations.value?.currency?.displayLabel, - principalAmount = String.format( - Locale.getDefault(), - "%.2f", - loanWithAssociations.value?.principal, - ), - submittedDate = DateHelper.getDateAsString( - loanWithAssociations.value?.timeline?.submittedOnDate, - "dd-MM-yyyy", - ), - disbursementDate = DateHelper.getDateAsString( - loanWithAssociations.value?.timeline?.expectedDisbursementDate, - "dd-MM-yyyy", - ), - ) - } - } - - private fun showLoanTemplateByProduct(loanTemplate: LoanTemplate) { - val loanPurposeList = refreshLoanPurposeList(loanTemplate = loanTemplate) - _loanApplicationScreenData.update { - it.copy( - listLoanPurpose = loanPurposeList, - selectedLoanPurpose = loanPurposeList[0], - accountNumber = loanTemplate.clientAccountNo, - clientName = loanTemplate.clientName, - currencyLabel = loanTemplate.currency?.displayLabel, - principalAmount = String.format( - Locale.getDefault(), - "%.2f", - loanTemplate.principal, - ), - ) - } - } - - private fun showUpdateLoanTemplateByProduct(loanTemplate: LoanTemplate) { - val loanPurposeList = refreshLoanPurposeList(loanTemplate = loanTemplate) - if (isLoanUpdatePurposesInitialization && loanWithAssociations.value?.loanPurposeName != null) { - _loanApplicationScreenData.update { - it.copy( - listLoanPurpose = loanPurposeList, - selectedLoanPurpose = loanPurposeList[0], - ) - } - } else { - _loanApplicationScreenData.update { - it.copy( - listLoanPurpose = loanPurposeList, - selectedLoanPurpose = loanWithAssociations.value?.loanPurposeName, - accountNumber = loanTemplate.clientAccountNo, - clientName = loanTemplate.clientName, - currencyLabel = loanTemplate.currency?.displayLabel, - principalAmount = String.format( - Locale.getDefault(), - "%.2f", - loanTemplate.principal, - ), - ) - } - } - } - - private fun refreshLoanPurposeList(loanTemplate: LoanTemplate): MutableList { - val loanPurposeList = mutableListOf() - loanPurposeList.add("Purpose not provided") - for (loanPurposeOptions in loanTemplate.loanPurposeOptions) { - loanPurposeList.add(loanPurposeOptions.name) - } - return loanPurposeList - } - - private fun refreshLoanProductList(loanTemplate: LoanTemplate): List { - val loanProductList = _loanApplicationScreenData.value.listLoanProducts.toMutableList() - for ((_, name) in loanTemplate.productOptions) { - if (!loanProductList.contains(name)) { - loanProductList.add(name) - } - } - return loanProductList - } - - fun productSelected(position: Int) { - productId = loanTemplate.productOptions[position].id ?: 0 - loadLoanApplicationTemplateByProduct(productId, loanState.value) - _loanApplicationScreenData.update { - it.copy(selectedLoanProduct = loanApplicationScreenData.value.listLoanProducts[position]) - } - } - - fun purposeSelected(position: Int) { - loanTemplate.loanPurposeOptions.let { - if (it.size > position) { - purposeId = loanTemplate.loanPurposeOptions[position].id ?: 0 - } - } - _loanApplicationScreenData.update { - it.copy(selectedLoanPurpose = loanApplicationScreenData.value.listLoanPurpose[position]) - } - } - - fun setDisburseDate(date: String) { - _loanApplicationScreenData.update { it.copy(disbursementDate = date) } - } - - fun setPrincipalAmount(amount: String) { - _loanApplicationScreenData.update { it.copy(principalAmount = amount) } - } -} - -internal data class LoanApplicationScreenData( - var accountNumber: String? = null, - var clientName: String? = null, - var listLoanProducts: List = listOf(), - var selectedLoanProduct: String? = null, - var listLoanPurpose: List = listOf(), - var selectedLoanPurpose: String? = null, - var principalAmount: String? = null, - var currencyLabel: String? = null, - var selectedDisbursementDate: Instant? = null, - var disbursementDate: String? = null, - var submittedDate: String? = null, -) - -internal sealed class LoanApplicationUiState { - data object Loading : LoanApplicationUiState() - data object Success : LoanApplicationUiState() - data class Error(val errorMessageId: Int) : LoanApplicationUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt deleted file mode 100644 index 2de072ca2..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountSummary/LoanAccountSummaryViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountSummary - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.feature.loan.loanAccountSummary.LoanAccountSummaryUiState.Loading -import javax.inject.Inject - -@HiltViewModel -internal class LoanAccountSummaryViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - - val loanUiState = savedStateHandle.getStateFlow( - key = Constants.LOAN_ID, - initialValue = null, - ).flatMapLatest { - loanRepositoryImp.getLoanWithAssociations(Constants.REPAYMENT_SCHEDULE, it) - }.catch { - LoanAccountSummaryUiState.Error - }.map { - LoanAccountSummaryUiState.Success(it) - }.stateIn( - scope = viewModelScope, - initialValue = Loading, - started = SharingStarted.WhileSubscribed(), - ) -} - -internal sealed class LoanAccountSummaryUiState { - data object Loading : LoanAccountSummaryUiState() - data object Error : LoanAccountSummaryUiState() - data class Success(val loanWithAssociations: LoanWithAssociations?) : - LoanAccountSummaryUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt deleted file mode 100644 index 7c9e03221..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountTransaction/LoanAccountTransactionViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountTransaction - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.network.Result -import org.mifos.mobile.core.network.asResult -import javax.inject.Inject - -@HiltViewModel -internal class LoanAccountTransactionViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - val loanId = savedStateHandle.getStateFlow(key = Constants.LOAN_ID, initialValue = null) - - var loanUiState = loanId - .flatMapLatest { - loanRepositoryImp.getLoanWithAssociations(Constants.TRANSACTIONS, it) - } - .asResult() - .map { result -> - when (result) { - is Result.Success -> LoanAccountTransactionUiState.Success(result.data) - is Result.Loading -> LoanAccountTransactionUiState.Loading - is Result.Error -> LoanAccountTransactionUiState.Error - } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = LoanAccountTransactionUiState.Loading, - ) -} - -internal sealed class LoanAccountTransactionUiState { - data object Loading : LoanAccountTransactionUiState() - data object Error : LoanAccountTransactionUiState() - data class Success(val loanWithAssociations: LoanWithAssociations?) : - LoanAccountTransactionUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt deleted file mode 100644 index 1e278339f..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountWithdraw - -import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.designsystem.components.MifosButton -import org.mifos.mobile.core.designsystem.components.MifosTopBar -import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.ui.component.MifosErrorComponent -import org.mifos.mobile.core.ui.component.MifosProgressIndicator -import org.mifos.mobile.core.ui.component.MifosTitleDescSingleLineEqual -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R - -@Composable -internal fun LoanAccountWithdrawScreen( - navigateBack: () -> Unit, - modifier: Modifier = Modifier, - viewModel: LoanAccountWithdrawViewModel = hiltViewModel(), -) { - val uiState by viewModel.loanUiState.collectAsStateWithLifecycle() - val loanWithAssociations by viewModel.loanWithAssociations.collectAsStateWithLifecycle() - - LoanAccountWithdrawScreen( - uiState = uiState, - loanWithAssociations = loanWithAssociations, - navigateBack = navigateBack, - withdraw = viewModel::withdrawLoanAccount, - modifier = modifier, - ) -} - -@Composable -private fun LoanAccountWithdrawScreen( - uiState: LoanAccountWithdrawUiState, - loanWithAssociations: LoanWithAssociations?, - navigateBack: () -> Unit, - withdraw: (String) -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - Column( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - ) { - MifosTopBar( - navigateBack = { navigateBack() }, - title = { Text(text = stringResource(id = R.string.withdraw_loan)) }, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Box(modifier = Modifier.weight(1f)) { - LoanAccountWithdrawContent( - loanWithAssociations = loanWithAssociations, - withdraw = withdraw, - ) - - when (uiState) { - is LoanAccountWithdrawUiState.Loading -> { - MifosProgressIndicator( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), - ) - } - - is LoanAccountWithdrawUiState.Success -> { - Toast.makeText( - context, - R.string.loan_application_withdrawn_successfully, - Toast.LENGTH_SHORT, - ).show() - navigateBack() - } - - is LoanAccountWithdrawUiState.Error -> { - MifosErrorComponent(message = stringResource(id = uiState.messageId)) - } - - is LoanAccountWithdrawUiState.WithdrawUiReady -> Unit - } - } - } -} - -@Composable -private fun LoanAccountWithdrawContent( - loanWithAssociations: LoanWithAssociations?, - withdraw: (String) -> Unit, - modifier: Modifier = Modifier, -) { - var reason by remember { mutableStateOf(TextFieldValue("")) } - - Column( - modifier = modifier - .padding(16.dp) - .fillMaxSize(), - ) { - MifosTitleDescSingleLineEqual( - title = stringResource(id = R.string.client_name), - description = loanWithAssociations?.clientName ?: "", - ) - - Spacer(modifier = Modifier.height(8.dp)) - - MifosTitleDescSingleLineEqual( - title = stringResource(id = R.string.account_number), - description = loanWithAssociations?.accountNo ?: "", - ) - - Spacer(modifier = Modifier.height(36.dp)) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = reason, - placeholder = { - Text(text = stringResource(id = R.string.withdraw_loan_reason)) - }, - onValueChange = { reason = it }, - textStyle = MaterialTheme.typography.bodyLarge, - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.background, - unfocusedContainerColor = MaterialTheme.colorScheme.background, - ), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - MifosButton( - modifier = Modifier.fillMaxWidth(), - shape = RectangleShape, - onClick = { withdraw(reason.text) }, - content = { - Text( - modifier = Modifier.padding(6.dp), - text = stringResource(id = R.string.withdraw_loan), - style = MaterialTheme.typography.titleMedium, - ) - }, - ) - } -} - -internal class UiStatesParameterProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - LoanAccountWithdrawUiState.WithdrawUiReady, - LoanAccountWithdrawUiState.Error(messageId = R.string.something_went_wrong), - LoanAccountWithdrawUiState.Loading, - ) -} - -@DevicePreviews -@Composable -private fun LoanAccountWithdrawPreview( - @PreviewParameter(UiStatesParameterProvider::class) - loanAccountWithdrawUiState: LoanAccountWithdrawUiState, -) { - MifosMobileTheme { - LoanAccountWithdrawScreen( - uiState = loanAccountWithdrawUiState, - loanWithAssociations = LoanWithAssociations( - clientName = "Mifos Mobile", - accountNo = "0001", - ), - navigateBack = {}, - withdraw = {}, - ) - } -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt deleted file mode 100644 index a2e4c169c..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanAccountWithdraw/LoanAccountWithdrawViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanAccountWithdraw - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.common.utils.DateHelper -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithdraw -import org.mifos.mobile.feature.loan.R -import javax.inject.Inject - -@HiltViewModel -internal class LoanAccountWithdrawViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - - private val _loanUiState = - MutableStateFlow(LoanAccountWithdrawUiState.WithdrawUiReady) - val loanUiState: StateFlow = _loanUiState - - val loanId = savedStateHandle.getStateFlow(key = Constants.LOAN_ID, initialValue = null) - - val loanWithAssociations: StateFlow = loanId - .flatMapLatest { - loanRepositoryImp.getLoanWithAssociations(Constants.TRANSACTIONS, it) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = null, - ) - - fun withdrawLoanAccount(loanReason: String?) { - val loanWithdraw = LoanWithdraw().apply { - note = loanReason - withdrawnOnDate = DateHelper - .getDateAsStringFromLong(System.currentTimeMillis()) - } - - viewModelScope.launch { - _loanUiState.value = LoanAccountWithdrawUiState.Loading - loanRepositoryImp.withdrawLoanAccount( - loanWithAssociations.value?.id?.toLong(), - loanWithdraw, - ) - ?.catch { - _loanUiState.value = - LoanAccountWithdrawUiState.Error(R.string.error_loan_account_withdraw) - }?.collect { - _loanUiState.value = LoanAccountWithdrawUiState.Success - } - } - } -} - -internal sealed class LoanAccountWithdrawUiState { - data object WithdrawUiReady : LoanAccountWithdrawUiState() - data object Loading : LoanAccountWithdrawUiState() - data object Success : LoanAccountWithdrawUiState() - data class Error(val messageId: Int) : LoanAccountWithdrawUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt deleted file mode 100644 index 1dcebf9ed..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanRepaymentSchedule - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.data.repository.LoanRepository -import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations -import org.mifos.mobile.feature.loan.R -import javax.inject.Inject - -@HiltViewModel -internal class LoanRepaymentScheduleViewModel @Inject constructor( - private val loanRepositoryImp: LoanRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - private val loanId = savedStateHandle.getStateFlow( - key = Constants.LOAN_ID, - null, - ) - - private val mOnRetry = MutableStateFlow(false) - - val loanUiState = loanId.combine(mOnRetry) { loanId, onRetry -> - loanId to onRetry - }.flatMapLatest { - loanRepositoryImp.getLoanWithAssociations( - Constants.REPAYMENT_SCHEDULE, - loanId.value, - ) - }.catch { - LoanUiState.ShowError(R.string.repayment_schedule) - }.map { - if (it?.repaymentSchedule?.periods?.isNotEmpty() == true) { - LoanUiState.ShowLoan(it) - } else { - it?.let { it1 -> LoanUiState.ShowEmpty(it1) }!! - } - }.stateIn( - scope = viewModelScope, - initialValue = LoanUiState.Loading, - started = SharingStarted.WhileSubscribed(5000), - ) - - fun loanLoanWithAssociations() { - mOnRetry.update { !it } - } -} - -internal sealed class LoanUiState { - data object Loading : LoanUiState() - data class ShowError(val message: Int) : LoanUiState() - data class ShowLoan(val loanWithAssociations: LoanWithAssociations) : LoanUiState() - data class ShowEmpty(val loanWithAssociations: LoanWithAssociations) : LoanUiState() -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt deleted file mode 100644 index 62a2e496a..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanReview - -import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -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.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.mifos.mobile.core.common.Network -import org.mifos.mobile.core.designsystem.components.MifosTopBar -import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme -import org.mifos.mobile.core.model.enums.LoanState -import org.mifos.mobile.core.ui.component.MifosProgressIndicator -import org.mifos.mobile.core.ui.component.NoInternet -import org.mifos.mobile.core.ui.utils.DevicePreviews -import org.mifos.mobile.feature.loan.R - -@Composable -internal fun ReviewLoanApplicationScreen( - navigateBack: (isSuccess: Boolean) -> Unit, - modifier: Modifier = Modifier, - viewModel: ReviewLoanApplicationViewModel = hiltViewModel(), -) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val data by viewModel.reviewLoanApplicationUiData.collectAsStateWithLifecycle() - - ReviewLoanApplicationScreen( - uiState = uiState, - data = data, - navigateBack = navigateBack, - onSubmit = viewModel::submitLoan, - modifier = modifier, - ) -} - -@Composable -private fun ReviewLoanApplicationScreen( - uiState: ReviewLoanApplicationUiState, - data: ReviewLoanApplicationUiData, - navigateBack: (isSuccess: Boolean) -> Unit, - onSubmit: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - Column(modifier = modifier.fillMaxSize()) { - MifosTopBar( - modifier = Modifier.fillMaxWidth(), - navigateBack = { navigateBack(false) }, - title = { Text(text = stringResource(id = R.string.update_loan)) }, - ) - - Box(modifier = Modifier.weight(1f)) { - ReviewLoanApplicationContent( - data = data, - onSubmit = onSubmit, - modifier = Modifier.padding(16.dp), - ) - - when (uiState) { - is ReviewLoanApplicationUiState.Loading -> { - MifosProgressIndicator( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background.copy(0.8f)), - ) - } - - is ReviewLoanApplicationUiState.Error -> { - ErrorComponent(errorThrowable = uiState.throwable) - } - - is ReviewLoanApplicationUiState.Success -> { - when (uiState.loanState) { - LoanState.CREATE -> - LaunchedEffect(key1 = true) { - Toast.makeText( - context, - context.getString(R.string.loan_application_submitted_successfully), - Toast.LENGTH_SHORT, - ).show() - } - - LoanState.UPDATE -> - LaunchedEffect(key1 = true) { - Toast.makeText( - context, - context.getString(R.string.loan_application_updated_successfully), - Toast.LENGTH_SHORT, - ).show() - } - } - navigateBack(true) - } - - is ReviewLoanApplicationUiState.ReviewLoanUiReady -> Unit - } - } - } -} - -@Composable -private fun ErrorComponent( - errorThrowable: String?, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - - if (!Network.isConnected(context)) { - NoInternet( - error = R.string.no_internet_connection, - isRetryEnabled = false, - modifier = modifier, - ) - } else { - LaunchedEffect(errorThrowable) { - Toast.makeText( - context, - context.getString(R.string.something_went_wrong), - Toast.LENGTH_SHORT, - ).show() - } - } -} - -internal class UiStatesParameterProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - ReviewLoanApplicationUiState.ReviewLoanUiReady, - ReviewLoanApplicationUiState.Error(throwable = null), - ReviewLoanApplicationUiState.Loading, - ReviewLoanApplicationUiState.Success(loanState = LoanState.CREATE), - ) -} - -@DevicePreviews -@Composable -private fun ReviewLoanApplicationScreenPreview( - @PreviewParameter(UiStatesParameterProvider::class) - reviewLoanApplicationUiState: ReviewLoanApplicationUiState, -) { - MifosMobileTheme { - ReviewLoanApplicationScreen( - uiState = reviewLoanApplicationUiState, - data = ReviewLoanApplicationUiData(), - navigateBack = {}, - onSubmit = {}, - ) - } -} diff --git a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt b/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt deleted file mode 100644 index e236778f3..000000000 --- a/feature/loan/src/main/java/org/mifos/mobile/feature/loan/loanReview/ReviewLoanApplicationViewModel.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2024 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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.feature.loan.loanReview - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.gson.Gson -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import org.mifos.mobile.core.common.Constants -import org.mifos.mobile.core.common.Constants.LOANS_PAYLOAD -import org.mifos.mobile.core.data.repository.ReviewLoanApplicationRepository -import org.mifos.mobile.core.model.entity.payload.LoansPayload -import org.mifos.mobile.core.model.enums.LoanState -import org.mifos.mobile.feature.loan.loanReview.ReviewLoanApplicationUiState.Loading -import javax.inject.Inject - -@HiltViewModel -internal class ReviewLoanApplicationViewModel @Inject constructor( - private val reviewLoanApplicationRepositoryImpl: ReviewLoanApplicationRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - - private val mUiState = MutableStateFlow(Loading) - val uiState: StateFlow = mUiState.asStateFlow() - - private val loanId = - savedStateHandle.getStateFlow(key = Constants.LOAN_ID, initialValue = null) - private val loanState = - savedStateHandle.getStateFlow(key = Constants.LOAN_STATE, initialValue = LoanState.CREATE) - private val loanName = - savedStateHandle.getStateFlow(key = Constants.LOAN_NAME, initialValue = null) - private val accountNo = - savedStateHandle.getStateFlow(key = Constants.ACCOUNT_NUMBER, initialValue = null) - private val loansPayloadString = - savedStateHandle.getStateFlow(key = LOANS_PAYLOAD, initialValue = null) - - private val loansPayload: StateFlow = loansPayloadString - .map { jsonString -> - jsonString?.let { - Gson().fromJson(it, LoansPayload::class.java) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - - val reviewLoanApplicationUiData: StateFlow = combine( - loanId, - loanState, - loanName, - accountNo, - loansPayload, - ) { loanId, loanState, loanName, accountNo, loansPayload -> - ReviewLoanApplicationUiData( - loanState = loanState, - loanName = loanName, - accountNo = accountNo, - loanProduct = loansPayload?.productName, - loanPurpose = loansPayload?.loanPurpose, - principal = loansPayload?.principal, - currency = loansPayload?.currency, - submissionDate = loansPayload?.submittedOnDate, - disbursementDate = loansPayload?.expectedDisbursementDate, - loanId = loanId ?: 0, - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = ReviewLoanApplicationUiData(), - ) - - fun submitLoan() = viewModelScope.launch(Dispatchers.IO) { - mUiState.value = Loading - reviewLoanApplicationRepositoryImpl.submitLoan( - loanState = reviewLoanApplicationUiData.value.loanState, - loansPayload = loansPayload.value ?: LoansPayload(), - loanId = reviewLoanApplicationUiData.value.loanId, - ).catch { - mUiState.value = ReviewLoanApplicationUiState.Error(it.message) - }.collect { - mUiState.value = - ReviewLoanApplicationUiState.Success(reviewLoanApplicationUiData.value.loanState) - } - } -} - -internal sealed class ReviewLoanApplicationUiState { - data object ReviewLoanUiReady : ReviewLoanApplicationUiState() - data object Loading : ReviewLoanApplicationUiState() - data class Error(val throwable: String?) : ReviewLoanApplicationUiState() - data class Success(val loanState: LoanState) : ReviewLoanApplicationUiState() -} - -internal class ReviewLoanApplicationUiData( - val loanId: Long = 0, - val loanState: LoanState = LoanState.CREATE, - val accountNo: String? = null, - val loanName: String? = null, - val disbursementDate: String? = null, - val submissionDate: String? = null, - val currency: String? = null, - val principal: Double? = null, - val loanPurpose: String? = null, - val loanProduct: String? = null, -) diff --git a/feature/loan/src/main/res/values/colors.xml b/feature/loan/src/main/res/values/colors.xml deleted file mode 100644 index 44ee3c939..000000000 --- a/feature/loan/src/main/res/values/colors.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - #ffffffff - #000000 - #eaeaea - #ff14c416 - #ff8bf98a - #fff9ac06 - #FF87DBF9 - #fff9393c - #ffd1d1d1 - #8ad3da44 - #8ada6134 - #ff003fff - #FF0000 - #1C1C1C - - #B2C1C8 - - - #ff33b5e5 - - #33999999 - - #BB666666 - - #ff99cc00 - - #ffff4444 - - #ff0099cc - - #ff669900 - - #ffcc0000 - - #ffaa66cc - - #ffffbb33 - - #ffff8800 - - #ff00ddff - - #33CCCCCC - - - - - - - - #03A9F4 - #0288D1 - #B3E5FC - #FF4081 - #212121 - #757575 - #FFFFFF - #BDBDBD - #ffffff - @color/blue_light - #EEEEEE - #00000000 - - - - @color/blue_light - @color/green_light - @color/red_light - @color/orange_light - - - \ No newline at end of file diff --git a/feature/savings-account/src/commonMain/kotlin/org/mifos/mobile/feature/savingsaccount/viewmodel/SavingsAccountViewmodel.kt b/feature/savings-account/src/commonMain/kotlin/org/mifos/mobile/feature/savingsaccount/viewmodel/SavingsAccountViewmodel.kt index ac049d606..133af14be 100644 --- a/feature/savings-account/src/commonMain/kotlin/org/mifos/mobile/feature/savingsaccount/viewmodel/SavingsAccountViewmodel.kt +++ b/feature/savings-account/src/commonMain/kotlin/org/mifos/mobile/feature/savingsaccount/viewmodel/SavingsAccountViewmodel.kt @@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource +import org.mifos.mobile.core.common.Constants import org.mifos.mobile.core.data.repository.AccountsRepository import org.mifos.mobile.core.data.util.NetworkMonitor import org.mifos.mobile.core.datastore.UserPreferencesRepository import org.mifos.mobile.core.model.entity.accounts.savings.SavingAccount -import org.mifos.mobile.core.model.enums.AccountType import org.mifos.mobile.feature.savingsaccount.utils.AccountState import org.mifos.mobile.feature.savingsaccount.utils.FilterUtil @@ -184,7 +184,7 @@ class SavingsAccountViewmodel( _accountsUiState.value = AccountState.Loading accountsRepositoryImpl.loadAccounts( clientId = clientId, - accountType = AccountType.SAVINGS.name, + accountType = Constants.SAVINGS_ACCOUNTS, ).catch { _accountsUiState.value = AccountState.Error }.collect { clientAccounts -> diff --git a/feature/share-account/src/commonMain/kotlin/org/mifos/mobile/feature/shareaccount/viewmodel/ShareAccountViewModel.kt b/feature/share-account/src/commonMain/kotlin/org/mifos/mobile/feature/shareaccount/viewmodel/ShareAccountViewModel.kt index 9e296f18d..8bf07f87e 100644 --- a/feature/share-account/src/commonMain/kotlin/org/mifos/mobile/feature/shareaccount/viewmodel/ShareAccountViewModel.kt +++ b/feature/share-account/src/commonMain/kotlin/org/mifos/mobile/feature/shareaccount/viewmodel/ShareAccountViewModel.kt @@ -7,6 +7,8 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ +@file:Suppress("ktlint:standard:property-naming") + package org.mifos.mobile.feature.shareaccount.viewmodel import androidx.lifecycle.ViewModel @@ -19,11 +21,11 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource +import org.mifos.mobile.core.common.Constants import org.mifos.mobile.core.data.repository.AccountsRepository import org.mifos.mobile.core.data.util.NetworkMonitor import org.mifos.mobile.core.datastore.UserPreferencesRepository import org.mifos.mobile.core.model.entity.accounts.share.ShareAccount -import org.mifos.mobile.core.model.enums.AccountType import org.mifos.mobile.feature.shareaccount.utils.AccountState import org.mifos.mobile.feature.shareaccount.utils.FilterUtil @@ -63,7 +65,6 @@ class ShareAccountViewModel( ) /** Holds the current state of share accounts UI. */ - @Suppress("PropertyName") private val _accountsUiState = MutableStateFlow(AccountState.Loading) val accountUiState: StateFlow = _accountsUiState.asStateFlow() @@ -184,7 +185,7 @@ class ShareAccountViewModel( _accountsUiState.value = AccountState.Loading accountsRepositoryImpl.loadAccounts( clientId = clientId, - accountType = AccountType.SHARE.name, + accountType = Constants.SHARE_ACCOUNTS, ).catch { _accountsUiState.value = AccountState.Error }.collect { clientAccounts -> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c61af17b..7b1f8aae9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -86,7 +86,7 @@ ktorfit = "2.2.0" ktorfitKsp = "2.2.0-1.0.29" # Koin CMP Dependencies -koin = "4.0.1-RC1" +koin = "4.0.2" koinAnnotationsVersion = "1.4.0-RC4" # CMP Libraries diff --git a/settings.gradle.kts b/settings.gradle.kts index aaebdeb14..630790fe7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,7 @@ include(":core:qrcode") //include(":core:testing") // Feature Modules -//include(":feature:loan") +include(":feature:loan") //include(":feature:beneficiary") //include(":feature:savings") //include(":feature:guarantor")