diff --git a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt index b45e2a64755..a7cdd51c90e 100644 --- a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt @@ -11,6 +11,7 @@ import com.mifos.core.data.repository.ClientIdentifiersRepository import com.mifos.core.data.repository.CreateNewCenterRepository import com.mifos.core.data.repository.GroupDetailsRepository import com.mifos.core.data.repository.GroupsListRepository +import com.mifos.core.data.repository.LoanAccountRepository import com.mifos.core.data.repository.NewIndividualCollectionSheetRepository import com.mifos.core.data.repository.PathTrackingRepository import com.mifos.core.data.repository.ReportCategoryRepository @@ -24,6 +25,7 @@ import com.mifos.core.data.repository_imp.ClientIdentifiersRepositoryImp import com.mifos.core.data.repository_imp.CreateNewCenterRepositoryImp import com.mifos.core.data.repository_imp.GroupDetailsRepositoryImp import com.mifos.core.data.repository_imp.GroupsListRepositoryImpl +import com.mifos.core.data.repository_imp.LoanAccountRepositoryImp import com.mifos.core.data.repository_imp.NewIndividualCollectionSheetRepositoryImp import com.mifos.core.data.repository_imp.PathTrackingRepositoryImp import com.mifos.core.data.repository_imp.ReportCategoryRepositoryImp @@ -76,4 +78,7 @@ abstract class DataModule { @Binds internal abstract fun bindActivateRepository(impl: ActivateRepositoryImp): ActivateRepository + + @Binds + internal abstract fun bindLoanAccountRepository(impl: LoanAccountRepositoryImp): LoanAccountRepository } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepository.kt b/core/data/src/main/java/com/mifos/core/data/repository/LoanAccountRepository.kt similarity index 53% rename from mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepository.kt rename to core/data/src/main/java/com/mifos/core/data/repository/LoanAccountRepository.kt index 8c8c2e30af8..c1ecf66bb27 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepository.kt +++ b/core/data/src/main/java/com/mifos/core/data/repository/LoanAccountRepository.kt @@ -1,4 +1,4 @@ -package com.mifos.mifosxdroid.online.loanaccount +package com.mifos.core.data.repository import com.mifos.core.data.LoansPayload import com.mifos.core.objects.accounts.loan.Loans @@ -11,11 +11,9 @@ import rx.Observable */ interface LoanAccountRepository { - fun allLoans(): Observable> - - fun getLoansAccountTemplate(clientId: Int, productId: Int): Observable - - fun createLoansAccount(loansPayload: LoansPayload?): Observable + suspend fun allLoans(): Observable> + suspend fun getLoansAccountTemplate(clientId: Int, productId: Int): Observable + suspend fun createLoansAccount(loansPayload: LoansPayload): Observable } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepositoryImp.kt b/core/data/src/main/java/com/mifos/core/data/repository_imp/LoanAccountRepositoryImp.kt similarity index 63% rename from mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepositoryImp.kt rename to core/data/src/main/java/com/mifos/core/data/repository_imp/LoanAccountRepositoryImp.kt index 511316df17b..224e42b844b 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountRepositoryImp.kt +++ b/core/data/src/main/java/com/mifos/core/data/repository_imp/LoanAccountRepositoryImp.kt @@ -1,6 +1,7 @@ -package com.mifos.mifosxdroid.online.loanaccount +package com.mifos.core.data.repository_imp import com.mifos.core.data.LoansPayload +import com.mifos.core.data.repository.LoanAccountRepository import com.mifos.core.network.datamanager.DataManagerLoan import com.mifos.core.objects.accounts.loan.Loans import com.mifos.core.objects.organisation.LoanProducts @@ -14,15 +15,18 @@ import javax.inject.Inject class LoanAccountRepositoryImp @Inject constructor(private val dataManagerLoan: DataManagerLoan) : LoanAccountRepository { - override fun allLoans(): Observable> { + override suspend fun allLoans(): Observable> { return dataManagerLoan.allLoans } - override fun getLoansAccountTemplate(clientId: Int, productId: Int): Observable { + override suspend fun getLoansAccountTemplate( + clientId: Int, + productId: Int + ): Observable { return dataManagerLoan.getLoansAccountTemplate(clientId, productId) } - override fun createLoansAccount(loansPayload: LoansPayload?): Observable { + override suspend fun createLoansAccount(loansPayload: LoansPayload): Observable { return dataManagerLoan.createLoansAccount(loansPayload) } } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt index 35b13b5d9a0..2a8ce4be99a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt @@ -1,6 +1,7 @@ package com.mifos.core.designsystem.component import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.isSystemInDarkTheme @@ -23,9 +24,12 @@ 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.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction @@ -98,13 +102,68 @@ fun MifosOutlinedTextField( ) } +@Composable +fun MifosOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + maxLines: Int = 1, + singleLine: Boolean = true, + icon: ImageVector? = null, + label: String, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + error: Int? +) { + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + leadingIcon = if (icon != null) { + { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isSystemInDarkTheme()) White else DarkGray + ) + } + } else null, + trailingIcon = trailingIcon, + maxLines = maxLines, + singleLine = singleLine, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, + focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + visualTransformation = visualTransformation, + isError = error != null, + supportingText = if (error != null) { + { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = error), + color = MaterialTheme.colorScheme.error + ) + } + } else { + null + } + ) +} @Composable fun MifosOutlinedTextField( modifier: Modifier = Modifier, value: String, label: String, - leadingIcon: ImageVector, + leadingIcon: ImageVector? = null, maxLines: Int = 1, isError: Boolean = false, errorText: String? = null, @@ -146,7 +205,7 @@ fun MifosOutlinedTextField( ) }, leadingIcon = { - Icon(imageVector = leadingIcon, contentDescription = "leadingIcon") + leadingIcon?.let { Icon(imageVector = it, contentDescription = "leadingIcon") } }, trailingIcon = @Composable { if (isPasswordToggleDisplayed) { @@ -202,6 +261,62 @@ fun MifosOutlinedTextField( ) } +@Composable +fun MifosOutlinedTextField( + modifier: Modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + value: String, + onValueChange: (String) -> Unit, + maxLines: Int = 1, + singleLine: Boolean = true, + icon: ImageVector? = null, + label: String, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardType: KeyboardType = KeyboardType.Text, + error: Int? +) { + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + modifier = modifier, + leadingIcon = if (icon != null) { + { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isSystemInDarkTheme()) White else DarkGray + ) + } + } else null, + trailingIcon = trailingIcon, + maxLines = maxLines, + singleLine = singleLine, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, + focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next,keyboardType = keyboardType), + visualTransformation = visualTransformation, + isError = error != null, + supportingText = if (error != null) { + { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = error), + color = MaterialTheme.colorScheme.error + ) + } + } else { + null + } + ) +} + @Composable private fun PasswordToggleIcon( modifier: Modifier = Modifier, @@ -250,7 +365,7 @@ private fun ClearIconButton( ) } } - + } @Composable diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt index d439db15fc0..80cb75f0788 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt @@ -30,6 +30,9 @@ import com.mifos.core.designsystem.theme.BluePrimaryDark @Composable fun MifosTextFieldDropdown( + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), value: String, onValueChanged: (String) -> Unit, onOptionSelected: (Int, String) -> Unit, @@ -47,10 +50,7 @@ fun MifosTextFieldDropdown( value = value, onValueChange = { onValueChanged(it) }, label = { Text(text = stringResource(id = label)) }, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp) - .menuAnchor(), + modifier = modifier.menuAnchor(), maxLines = 1, colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, diff --git a/core/domain/src/main/java/com/mifos/core/domain/use_cases/CreateLoanAccountUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/use_cases/CreateLoanAccountUseCase.kt new file mode 100644 index 00000000000..a1bffdc2659 --- /dev/null +++ b/core/domain/src/main/java/com/mifos/core/domain/use_cases/CreateLoanAccountUseCase.kt @@ -0,0 +1,41 @@ +package com.mifos.core.domain.use_cases + +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.LoansPayload +import com.mifos.core.data.repository.LoanAccountRepository +import com.mifos.core.objects.accounts.loan.Loans +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject + +class CreateLoanAccountUseCase @Inject constructor(private val loanAccountRepository: LoanAccountRepository) { + + suspend operator fun invoke(loansPayload: LoansPayload): Flow> = callbackFlow { + try { + trySend(Resource.Loading()) + loanAccountRepository.createLoansAccount(loansPayload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber() { + override fun onCompleted() {} + + override fun onError(exception: Throwable) { + trySend(Resource.Error(exception.message.toString())) + } + + override fun onNext(loans: Loans) { + trySend(Resource.Success(loans)) + } + }) + + awaitClose { channel.close() } + + } catch (exception: Exception) { + trySend(Resource.Error(exception.message.toString())) + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetAllLoanUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetAllLoanUseCase.kt new file mode 100644 index 00000000000..48c12688927 --- /dev/null +++ b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetAllLoanUseCase.kt @@ -0,0 +1,38 @@ +package com.mifos.core.domain.use_cases + +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.repository.LoanAccountRepository +import com.mifos.core.objects.organisation.LoanProducts +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject + +class GetAllLoanUseCase @Inject constructor(private val loanAccountRepository: LoanAccountRepository) { + + suspend operator fun invoke(): Flow>> = callbackFlow { + try { + trySend(Resource.Loading()) + loanAccountRepository.allLoans() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber>() { + override fun onCompleted() {} + + override fun onError(exception: Throwable) { + trySend(Resource.Error(exception.message.toString())) + } + + override fun onNext(products: List) { + trySend(Resource.Success(products)) + } + }) + awaitClose { channel.close() } + } catch (exception: Exception) { + trySend(Resource.Error(exception.message.toString())) + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetLoansAccountTemplateUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetLoansAccountTemplateUseCase.kt new file mode 100644 index 00000000000..69e8916bbd9 --- /dev/null +++ b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetLoansAccountTemplateUseCase.kt @@ -0,0 +1,41 @@ +package com.mifos.core.domain.use_cases + +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.repository.LoanAccountRepository +import com.mifos.core.objects.templates.loans.LoanTemplate +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject + +class GetLoansAccountTemplateUseCase @Inject constructor(private val loanAccountRepository: LoanAccountRepository) { + + suspend operator fun invoke(clientId: Int, productId: Int): Flow> = + callbackFlow { + try { + trySend(Resource.Loading()) + loanAccountRepository.getLoansAccountTemplate(clientId, productId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber() { + override fun onCompleted() {} + + override fun onError(exception: Throwable) { + trySend(Resource.Error(exception.message.toString())) + } + + override fun onNext(loanTemplate: LoanTemplate?) { + trySend(Resource.Success(loanTemplate)) + } + }) + + awaitClose { channel.close() } + + } catch (exception: Exception) { + trySend(Resource.Error(exception.message.toString())) + } + } +} \ No newline at end of file diff --git a/feature/loan/.gitignore b/feature/loan/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/feature/loan/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/loan/build.gradle.kts b/feature/loan/build.gradle.kts new file mode 100644 index 00000000000..d4ce95f19f3 --- /dev/null +++ b/feature/loan/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.mifos.android.feature) + alias(libs.plugins.mifos.android.library.compose) + alias(libs.plugins.mifos.android.library.jacoco) +} + +android { + namespace = "com.mifos.feature.loan" +} + +dependencies { + + implementation(projects.core.domain) + + //DBFlow dependencies + kapt(libs.dbflow.processor) + implementation(libs.dbflow) + kapt(libs.github.dbflow.processor) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.core.testing) + + androidTestImplementation(projects.core.testing) +} \ No newline at end of file diff --git a/feature/loan/consumer-rules.pro b/feature/loan/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/feature/loan/proguard-rules.pro b/feature/loan/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/feature/loan/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/loan/src/androidTest/java/com/mifos/feature/loan/ExampleInstrumentedTest.kt b/feature/loan/src/androidTest/java/com/mifos/feature/loan/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..80841bc46e4 --- /dev/null +++ b/feature/loan/src/androidTest/java/com/mifos/feature/loan/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.mifos.feature.loan + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.mifos.feature.loan.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/loan/src/main/AndroidManifest.xml b/feature/loan/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/feature/loan/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountScreen.kt b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountScreen.kt new file mode 100644 index 00000000000..d8e05848d6f --- /dev/null +++ b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountScreen.kt @@ -0,0 +1,644 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.mifos.feature.loan.loan_account + +import android.widget.Toast +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.data.LoansPayload +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosDatePickerTextField +import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.BluePrimary +import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.objects.noncore.DataTable +import com.mifos.core.objects.organisation.LoanProducts +import com.mifos.core.objects.templates.loans.LoanTemplate +import com.mifos.feature.loan.R +import java.text.SimpleDateFormat +import java.util.Locale + +@Composable +fun LoanAccountScreen( + clientId: Int, + onBackPressed: () -> Unit, + dataTable: (List, LoansPayload) -> Unit +) { + val viewModel: LoanAccountViewModel = hiltViewModel() + val state by viewModel.loanAccountUiState.collectAsStateWithLifecycle() + val loanAccountTemplateState by viewModel.loanAccountTemplateUiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.loadAllLoans() + } + + LoanAccountScreen( + clientId = clientId, + state = state, + loanAccountTemplateState = loanAccountTemplateState, + onBackPressed = onBackPressed, + onRetry = { + viewModel.loadAllLoans() + }, + onLoanProductSelected = { productId -> + viewModel.loadLoanAccountTemplate(clientId, productId) + }, + createLoanAccount = { loansPayload -> + viewModel.createLoansAccount(loansPayload) + }, + dataTable = dataTable, + fetchTemplate = { productId -> + viewModel.loadLoanAccountTemplate(clientId, productId) + } + ) +} + +@Composable +fun LoanAccountScreen( + clientId: Int, + state: LoanAccountUiState, + loanAccountTemplateState: LoanTemplate, + onBackPressed: () -> Unit, + onRetry: () -> Unit, + onLoanProductSelected: (Int) -> Unit, + createLoanAccount: (LoansPayload) -> Unit, + dataTable: (List, LoansPayload) -> Unit, + fetchTemplate: (Int) -> Unit +) { + val snackbarHostState = remember { SnackbarHostState() } + + MifosScaffold( + icon = MifosIcons.arrowBack, + title = stringResource(id = R.string.feature_loan_application), + onBackPressed = onBackPressed, + snackbarHostState = snackbarHostState + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + ) { + when (state) { + is LoanAccountUiState.AllLoan -> { + LoanAccountContent( + clientsId = clientId, + productLoans = state.productLoans, + loanTemplate = loanAccountTemplateState, + onLoanProductSelected = onLoanProductSelected, + createLoanAccount = createLoanAccount, + dataTable = dataTable + ) + state.productLoans[0].id?.let { fetchTemplate(it) } + } + + is LoanAccountUiState.Error -> MifosSweetError(message = stringResource(id = state.message)) { + onRetry() + } + + is LoanAccountUiState.Loading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + MifosCircularProgress() + } + } + + is LoanAccountUiState.LoanAccountCreatedSuccessfully -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.feature_loan_account_created_successfully), + Toast.LENGTH_SHORT + ).show() + onBackPressed() + } + } + } + } +} + +@Composable +fun LoanAccountContent( + clientsId: Int, + productLoans: List, + loanTemplate: LoanTemplate, + onLoanProductSelected: (Int) -> Unit, + createLoanAccount: (LoansPayload) -> Unit, + dataTable: (List, LoansPayload) -> Unit +) { + + var selectedLoanProduct by rememberSaveable { mutableStateOf("") } + var selectedLoanProductId by rememberSaveable { mutableIntStateOf(0) } + var selectedLoanPurpose by rememberSaveable { mutableStateOf("") } + var selectedLoanPurposeId by rememberSaveable { mutableIntStateOf(0) } + var selectedLoanOfficer by rememberSaveable { mutableStateOf("") } + var selectedLoanOfficerId by rememberSaveable { mutableIntStateOf(0) } + var selectedFund by rememberSaveable { mutableStateOf("") } + var selectedFundId by rememberSaveable { mutableIntStateOf(0) } + + var showSubmissionDatePicker by rememberSaveable { mutableStateOf(false) } + var submissionDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val submissionDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = submissionDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + + var showDisbursementDatePicker by rememberSaveable { mutableStateOf(false) } + var disbursementDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val disbursementDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = disbursementDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + + var externalId by rememberSaveable { mutableStateOf("") } + var principalAmount by rememberSaveable { mutableStateOf("10000.0") } + var numberOfRepayment by rememberSaveable { mutableStateOf("10") } + var nominal by rememberSaveable { mutableStateOf("5.0") } + var repaidEvery by rememberSaveable { mutableStateOf("2") } + var repaidEveryType by rememberSaveable { mutableStateOf("") } + var repaidEveryTypeFrequency by rememberSaveable { mutableIntStateOf(0) } + var loanTerms by rememberSaveable { mutableStateOf("20") } + var loanTermsType by rememberSaveable { mutableStateOf("") } + var loanTermsTypeFrequency by rememberSaveable { mutableIntStateOf(0) } + + var selectedLinkSavings by rememberSaveable { mutableStateOf("") } + var selectedLinkSavingsId by rememberSaveable { mutableIntStateOf(0) } + var selectedAmortization by rememberSaveable { mutableStateOf("") } + var selectedAmortizationId by rememberSaveable { mutableIntStateOf(0) } + var selectedInterestCalculationPeriod by rememberSaveable { mutableStateOf("") } + var selectedInterestCalculationPeriodId by rememberSaveable { mutableIntStateOf(0) } + var selectedRepaymentStrategy by rememberSaveable { mutableStateOf("") } + var selectedRepaymentStrategyId by rememberSaveable { mutableIntStateOf(0) } + var selectedInterestTypeMethod by rememberSaveable { mutableStateOf("") } + var selectedInterestTypeMethodId by rememberSaveable { mutableIntStateOf(0) } + var selectedCalculateExactDaysIn by rememberSaveable { mutableStateOf(false) } + + if (showSubmissionDatePicker) { + DatePickerDialog( + onDismissRequest = { + showSubmissionDatePicker = false + }, + confirmButton = { + TextButton( + onClick = { + showSubmissionDatePicker = false + submissionDatePickerState.selectedDateMillis?.let { + submissionDate = it + } + } + ) { Text(stringResource(id = R.string.feature_loan_select)) } + }, + dismissButton = { + TextButton( + onClick = { + showSubmissionDatePicker = false + } + ) { Text(stringResource(id = R.string.feature_loan_cancel)) } + } + ) + { + DatePicker(state = submissionDatePickerState) + } + } + + if (showDisbursementDatePicker) { + DatePickerDialog( + onDismissRequest = { + showDisbursementDatePicker = false + }, + confirmButton = { + TextButton( + onClick = { + showDisbursementDatePicker = false + disbursementDatePickerState.selectedDateMillis?.let { + disbursementDate = it + } + } + ) { Text(stringResource(id = R.string.feature_loan_select)) } + }, + dismissButton = { + TextButton( + onClick = { + showSubmissionDatePicker = false + } + ) { Text(stringResource(id = R.string.feature_loan_cancel)) } + } + ) + { + DatePicker(state = disbursementDatePickerState) + } + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + + MifosTextFieldDropdown( + value = selectedLoanProduct, + onValueChanged = { selectedLoanProduct = it }, + onOptionSelected = { index, value -> + selectedLoanProduct = value + productLoans[index].id?.let { + selectedLoanProductId = it + onLoanProductSelected(it) + } + }, + label = R.string.feature_loan_product, + options = productLoans.map { it.name.toString() }, + readOnly = true + ) + + MifosTextFieldDropdown( + value = selectedLoanPurpose, + onValueChanged = { selectedLoanPurpose = it }, + onOptionSelected = { index, value -> + selectedLoanPurpose = value + loanTemplate.loanPurposeOptions[index].id?.let { + selectedLoanPurposeId = it + } + }, + label = R.string.feature_loan_purpose, + options = loanTemplate.loanPurposeOptions.map { it.name.toString() }, + readOnly = true + ) + + MifosTextFieldDropdown( + value = selectedLoanOfficer, + onValueChanged = { selectedLoanOfficer = it }, + onOptionSelected = { index, value -> + selectedLoanOfficer = value + loanTemplate.loanOfficerOptions[index].id?.let { + selectedLoanOfficerId = it + } + }, + label = R.string.feature_loan_officer, + options = loanTemplate.loanOfficerOptions.map { it.displayName.toString() }, + readOnly = true + ) + + MifosTextFieldDropdown( + value = selectedFund, + onValueChanged = { selectedFund = it }, + onOptionSelected = { index, value -> + selectedFund = value + loanTemplate.fundOptions[index].id?.let { + selectedFundId = it + } + }, + label = R.string.feature_loan_fund, + options = loanTemplate.fundOptions.map { it.name.toString() }, + readOnly = true + ) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + submissionDate + ), + label = R.string.feature_loan_submission_date, + openDatePicker = { + showSubmissionDatePicker = true + } + ) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + disbursementDate + ), + label = R.string.feature_loan_disbursed_date, + openDatePicker = { + showDisbursementDatePicker = true + } + ) + + MifosOutlinedTextField( + value = externalId, + label = stringResource(id = R.string.feature_loan_external_id), + onValueChange = { + externalId = it + }, + error = null, + keyboardType = KeyboardType.Text + ) + + MifosTextFieldDropdown( + value = selectedLinkSavings, + onValueChanged = { selectedLinkSavings = it }, + onOptionSelected = { index, value -> + selectedLinkSavings = value + loanTemplate.accountLinkingOptions[index].id?.let { + selectedLinkSavingsId = it + } + }, + label = R.string.feature_loan_link_savings, + options = loanTemplate.accountLinkingOptions.map { it.productName.toString() }, + readOnly = true + ) + + MifosOutlinedTextField( + value = principalAmount, + label = stringResource(id = R.string.feature_loan_principal), + onValueChange = { + principalAmount = it + }, + error = null, + keyboardType = KeyboardType.Number + ) + + MifosOutlinedTextField( + value = numberOfRepayment, + label = stringResource(id = R.string.feature_loan_number_of_repayments), + onValueChange = { + numberOfRepayment = it + }, + error = null, + keyboardType = KeyboardType.Number + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + MifosOutlinedTextField( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + value = nominal, + label = stringResource(id = R.string.feature_loan_nominal), + onValueChange = { + nominal = it + }, + error = null, + keyboardType = KeyboardType.Number + ) + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(id = R.string.feature_loan_per_month) + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + MifosOutlinedTextField( + modifier = Modifier + .weight(2f) + .padding(start = 16.dp), + value = repaidEvery, + label = stringResource(id = R.string.feature_loan_repaid_every), + onValueChange = { + repaidEvery = it + }, + error = null, + keyboardType = KeyboardType.Number + ) + MifosTextFieldDropdown( + modifier = Modifier + .width(164.dp) + .padding(start = 8.dp, end = 16.dp), + value = repaidEveryType, + onValueChanged = { repaidEveryType = it }, + onOptionSelected = { index, value -> + repaidEveryType = value + loanTemplate.repaymentFrequencyDaysOfWeekTypeOptions[index].id?.let { + repaidEveryTypeFrequency = it + } + }, + label = R.string.feature_loan_term, + options = loanTemplate.repaymentFrequencyDaysOfWeekTypeOptions.map { it.value.toString() }, + readOnly = true + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + MifosOutlinedTextField( + modifier = Modifier + .weight(2f) + .padding(start = 16.dp), + value = loanTerms, + label = stringResource(id = R.string.feature_loan_loan_terms), + onValueChange = { + loanTerms = it + }, + error = null, + keyboardType = KeyboardType.Number + ) + MifosTextFieldDropdown( + modifier = Modifier + .width(164.dp) + .padding(start = 8.dp, end = 16.dp), + value = loanTermsType, + onValueChanged = { loanTermsType = it }, + onOptionSelected = { index, value -> + loanTermsType = value + loanTemplate.termFrequencyTypeOptions[index].id?.let { + loanTermsTypeFrequency = it + } + }, + label = R.string.feature_loan_term, + options = loanTemplate.termFrequencyTypeOptions.map { it.value.toString() }, + readOnly = true + ) + } + + MifosTextFieldDropdown( + value = selectedAmortization, + onValueChanged = { selectedAmortization = it }, + onOptionSelected = { index, value -> + selectedAmortization = value + loanTemplate.amortizationTypeOptions[index].id?.let { + selectedAmortizationId = it + } + }, + label = R.string.feature_loan_amortization, + options = loanTemplate.amortizationTypeOptions.map { it.value.toString() }, + readOnly = true + ) + + MifosTextFieldDropdown( + value = selectedInterestCalculationPeriod, + onValueChanged = { selectedInterestCalculationPeriod = it }, + onOptionSelected = { index, value -> + selectedInterestCalculationPeriod = value + loanTemplate.interestCalculationPeriodTypeOptions[index].id?.let { + selectedInterestCalculationPeriodId = it + } + }, + label = R.string.feature_loan_interest_calculation_period, + options = loanTemplate.interestCalculationPeriodTypeOptions.map { it.value.toString() }, + readOnly = true + ) + + MifosTextFieldDropdown( + value = selectedRepaymentStrategy, + onValueChanged = { selectedRepaymentStrategy = it }, + onOptionSelected = { index, value -> + selectedRepaymentStrategy = value + loanTemplate.transactionProcessingStrategyOptions[index].id?.let { + selectedRepaymentStrategyId = it + } + }, + label = R.string.feature_loan_repayment_strategy, + options = loanTemplate.transactionProcessingStrategyOptions.map { it.name.toString() }, + readOnly = true + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = selectedCalculateExactDaysIn, + onCheckedChange = { + selectedCalculateExactDaysIn = selectedCalculateExactDaysIn.not() + }) + Text(text = stringResource(id = R.string.feature_loan_calculate_interest_for_exact_days_in)) + } + + MifosTextFieldDropdown( + value = selectedInterestTypeMethod, + onValueChanged = { selectedInterestTypeMethod = it }, + onOptionSelected = { index, value -> + selectedInterestTypeMethod = value + loanTemplate.interestTypeOptions[index].id?.let { + selectedInterestTypeMethodId = it + } + }, + label = R.string.feature_loan_interest_type_method, + options = loanTemplate.interestTypeOptions.map { it.value.toString() }, + readOnly = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + val loadPayload = LoansPayload().apply { + allowPartialPeriodInterestCalcualtion = selectedCalculateExactDaysIn + amortizationType = selectedAmortizationId + clientId = clientsId + dateFormat = "dd MMMM yyyy" + expectedDisbursementDate = + SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + disbursementDate + ) + interestCalculationPeriodType = selectedInterestCalculationPeriodId + loanType = "individual" + locale = "en" + numberOfRepayments = numberOfRepayment.toInt() + principal = principalAmount.toDouble() + productId = selectedLoanProductId + repaymentEvery = repaidEvery.toInt() + submittedOnDate = + SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(submissionDate) + loanPurposeId = selectedLoanPurposeId + loanTermFrequency = loanTerms.toInt() + loanTermFrequencyType = loanTermsTypeFrequency + repaymentFrequencyType = loanTermsTypeFrequency + repaymentFrequencyDayOfWeekType = repaidEveryTypeFrequency + repaymentFrequencyNthDayType = repaidEveryTypeFrequency + transactionProcessingStrategyId = selectedRepaymentStrategyId + fundId = selectedFundId + interestType = selectedInterestTypeMethodId + loanOfficerId = selectedLoanOfficerId + linkAccountId = selectedLinkSavingsId + interestRatePerPeriod = nominal.toDouble() + } + if (loanTemplate.dataTables.size > 0) { + dataTable(loanTemplate.dataTables, loadPayload) + } else { + createLoanAccount(loadPayload) + } + }, + modifier = Modifier + .fillMaxWidth() + .heightIn(44.dp) + .padding(start = 16.dp, end = 16.dp), + contentPadding = PaddingValues(), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ) + ) { + Text(text = stringResource(id = R.string.feature_loan_submit), fontSize = 16.sp) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +class LoanAccountUiStateProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + LoanAccountUiState.AllLoan(sampleLoanList), + LoanAccountUiState.Error(R.string.feature_loan_application), + LoanAccountUiState.Loading, + LoanAccountUiState.LoanAccountCreatedSuccessfully + ) + +} + +@Preview(showBackground = true) +@Composable +private fun LoanAccountScreenPreview( + @PreviewParameter(LoanAccountUiStateProvider::class) state: LoanAccountUiState +) { + LoanAccountScreen( + clientId = 1, + state = state, + loanAccountTemplateState = LoanTemplate(), + onBackPressed = {}, + onRetry = {}, + onLoanProductSelected = {}, + createLoanAccount = {}, + dataTable = { _, _ -> }, + fetchTemplate = {} + ) +} + +val sampleLoanList = List(10) { + LoanProducts(name = "Loan $it", id = it) +} \ No newline at end of file diff --git a/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountUiState.kt b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountUiState.kt new file mode 100644 index 00000000000..6437dc42206 --- /dev/null +++ b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountUiState.kt @@ -0,0 +1,19 @@ +package com.mifos.feature.loan.loan_account + +import com.mifos.core.objects.accounts.loan.Loans +import com.mifos.core.objects.organisation.LoanProducts +import com.mifos.core.objects.templates.loans.LoanTemplate + +/** + * Created by Aditya Gupta on 08/08/23. + */ +sealed class LoanAccountUiState { + + data object Loading : LoanAccountUiState() + + data class AllLoan(val productLoans: List) : LoanAccountUiState() + + data class Error(val message: Int) : LoanAccountUiState() + + data object LoanAccountCreatedSuccessfully : LoanAccountUiState() +} \ No newline at end of file diff --git a/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountViewModel.kt b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountViewModel.kt new file mode 100644 index 00000000000..2fd54f9bb2b --- /dev/null +++ b/feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountViewModel.kt @@ -0,0 +1,75 @@ +package com.mifos.feature.loan.loan_account + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.LoansPayload +import com.mifos.core.domain.use_cases.CreateLoanAccountUseCase +import com.mifos.core.domain.use_cases.GetAllLoanUseCase +import com.mifos.core.domain.use_cases.GetLoansAccountTemplateUseCase +import com.mifos.core.objects.templates.loans.LoanTemplate +import com.mifos.feature.loan.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoanAccountViewModel @Inject constructor( + private val getAllLoanUseCase: GetAllLoanUseCase, + private val getLoansAccountTemplateUseCase: GetLoansAccountTemplateUseCase, + private val createLoanAccountUseCase: CreateLoanAccountUseCase +) : ViewModel() { + + private val _loanAccountUiState = + MutableStateFlow(LoanAccountUiState.Loading) + val loanAccountUiState = _loanAccountUiState.asStateFlow() + + private val _loanAccountTemplateUiState = MutableStateFlow(LoanTemplate()) + val loanAccountTemplateUiState = _loanAccountTemplateUiState.asStateFlow() + + fun loadAllLoans() = viewModelScope.launch(Dispatchers.IO) { + getAllLoanUseCase().collect { result -> + when (result) { + is Resource.Error -> _loanAccountUiState.value = + LoanAccountUiState.Error(R.string.feature_loan_failed_to_load_loan) + + is Resource.Loading -> _loanAccountUiState.value = LoanAccountUiState.Loading + + is Resource.Success -> _loanAccountUiState.value = + LoanAccountUiState.AllLoan(result.data ?: emptyList()) + } + } + } + + fun loadLoanAccountTemplate(clientId: Int, productId: Int) = + viewModelScope.launch(Dispatchers.IO) { + getLoansAccountTemplateUseCase(clientId, productId).collect { result -> + when (result) { + is Resource.Error -> _loanAccountUiState.value = + LoanAccountUiState.Error(R.string.feature_loan_failed_to_load_template) + + is Resource.Loading -> Unit + + is Resource.Success -> _loanAccountTemplateUiState.value = + result.data ?: LoanTemplate() + } + } + } + + fun createLoansAccount(loansPayload: LoansPayload) = viewModelScope.launch(Dispatchers.IO) { + createLoanAccountUseCase(loansPayload).collect { result -> + when (result) { + is Resource.Error -> _loanAccountUiState.value = + LoanAccountUiState.Error(R.string.feature_loan_failed_to_create_loan_account) + + is Resource.Loading -> _loanAccountUiState.value = LoanAccountUiState.Loading + + is Resource.Success -> _loanAccountUiState.value = + LoanAccountUiState.LoanAccountCreatedSuccessfully + } + } + } +} \ No newline at end of file diff --git a/feature/loan/src/main/res/values/strings.xml b/feature/loan/src/main/res/values/strings.xml new file mode 100644 index 00000000000..1203121d9ea --- /dev/null +++ b/feature/loan/src/main/res/values/strings.xml @@ -0,0 +1,37 @@ + + + Loan Application + Loan account created successfully + + Loan Product + Loan Purpose + Loan Officer + Fund + Submission Date + Disbursed Date + Select + Cancel + + External ID + Principal + Number of Repayments + Nominal + Repaid Every + Loan Terms + + Link Savings + Per month + Term + Amortization + Interest Calculation Period + Repayment Strategy + Interest Type Method + Calculate Interest for exact days in + Submit + + + + Failed to load loan + Failed to load loan template + Failed to create loan account + \ No newline at end of file diff --git a/feature/loan/src/test/java/com/mifos/feature/loan/ExampleUnitTest.kt b/feature/loan/src/test/java/com/mifos/feature/loan/ExampleUnitTest.kt new file mode 100644 index 00000000000..c189c45bb94 --- /dev/null +++ b/feature/loan/src/test/java/com/mifos/feature/loan/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.mifos.feature.loan + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 5691186de4c..24b2670f242 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { implementation(projects.feature.report) implementation(projects.feature.pathTracking) implementation(projects.feature.activate) + implementation(projects.feature.loan) implementation(projects.core.common) implementation(projects.core.ui) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt index c5f830c14c5..22ecfd89406 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt @@ -89,8 +89,6 @@ import com.mifos.mifosxdroid.online.grouploanaccount.GroupLoanAccountRepository import com.mifos.mifosxdroid.online.grouploanaccount.GroupLoanAccountRepositoryImp import com.mifos.mifosxdroid.online.groupslist.GroupsListRepository import com.mifos.mifosxdroid.online.groupslist.GroupsListRepositoryImp -import com.mifos.mifosxdroid.online.loanaccount.LoanAccountRepository -import com.mifos.mifosxdroid.online.loanaccount.LoanAccountRepositoryImp import com.mifos.mifosxdroid.online.loanaccountapproval.LoanAccountApprovalRepository import com.mifos.mifosxdroid.online.loanaccountapproval.LoanAccountApprovalRepositoryImp import com.mifos.mifosxdroid.online.loanaccountdisbursement.LoanAccountDisbursementRepository @@ -227,11 +225,6 @@ class RepositoryModule { return SavingsAccountRepositoryImp(dataManagerSavings) } - @Provides - fun providesLoanAccountRepository(dataManagerLoan: DataManagerLoan): LoanAccountRepository { - return LoanAccountRepositoryImp(dataManagerLoan) - } - @Provides fun providesSignatureRepository(dataManagerDocument: DataManagerDocument): SignatureRepository { return SignatureRepositoryImp(dataManagerDocument) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt index 84e2b7a1642..03531228f30 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt @@ -1,231 +1,41 @@ -/* - * This project is licensed under the open source MPL V2. - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ package com.mifos.mifosxdroid.online.createnewgroup import android.content.Intent import android.os.Bundle -import android.text.TextUtils +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import androidx.lifecycle.ViewModelProvider +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.mifos.core.common.utils.Constants -import com.mifos.core.objects.group.GroupPayload -import com.mifos.core.objects.organisation.Office import com.mifos.core.objects.response.SaveResponse -import com.mifos.exceptions.InvalidTextInputException -import com.mifos.exceptions.RequiredFieldException -import com.mifos.exceptions.ShortOfLengthException -import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.core.ProgressableFragment -import com.mifos.mifosxdroid.core.util.Toaster -import com.mifos.mifosxdroid.databinding.FragmentCreateNewGroupBinding +import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.online.GroupsActivity -import com.mifos.utils.DatePickerConstrainType -import com.mifos.utils.FragmentConstants -import com.mifos.utils.MifosResponseHandler -import com.mifos.utils.Network import com.mifos.utils.PrefManager -import com.mifos.utils.ValidationUtil -import com.mifos.utils.getDatePickerDialog -import com.mifos.utils.getTodayFormatted import dagger.hilt.android.AndroidEntryPoint -import java.text.SimpleDateFormat -import java.time.Instant -import java.util.Locale +import java.lang.reflect.InvocationTargetException -/** - * Created by nellyk on 1/22/2016. - */ //TODO Show Image and Text after successful or Failed during creation of Group and -//TODO A button to Continue or Finish the GroupCreation. @AndroidEntryPoint -class CreateNewGroupFragment : ProgressableFragment() { - - private lateinit var binding: FragmentCreateNewGroupBinding - - private lateinit var viewModel: CreateNewGroupViewModel - - private var activationDateString: String? = null - var officeId: Int? = 0 - var result = true - private var dateofsubmissionstring: String? = null - private val mListOffices: MutableList = ArrayList() - private var officeList: List = ArrayList() - - private var submissionDate: Instant = Instant.now() - private val submissionDatePickerDialog by lazy { - getDatePickerDialog(submissionDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { - val formattedDate = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(it) - submissionDate = Instant.ofEpochMilli(it) - binding.submittedDateFieldContainer.editText?.setText(formattedDate) - dateofsubmissionstring = binding.submittedDateFieldContainer.editText.toString() - } - } - private var activationDate: Instant = Instant.now() - private val activationDatePickerDialog by lazy { - getDatePickerDialog(activationDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { - val formattedDate = SimpleDateFormat("dd MM yyyy", Locale.getDefault()).format(it) - activationDate = Instant.ofEpochMilli(it) - binding.activateDateFieldContainer.editText?.setText(formattedDate) - activationDateString = binding.activateDateFieldContainer.editText.toString() - } - } - +class CreateNewGroupFragment : MifosBaseFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentCreateNewGroupBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[CreateNewGroupViewModel::class.java] - inflateSubmissionDate() - inflateActivationDate() - viewModel.loadOffices() - - //client active checkbox onCheckedListener - dateofsubmissionstring = getTodayFormatted() - binding.submittedDateFieldContainer.editText?.setText(getTodayFormatted()) - - activationDateString = getTodayFormatted() - binding.activateDateFieldContainer.editText?.setText(getTodayFormatted()) - - viewModel.createNewGroupUiState.observe(viewLifecycleOwner) { - when (it) { - is CreateNewGroupUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { - showProgressbar(false) - showGroupCreatedSuccessfully(it.saveResponse) - } - - is CreateNewGroupUiState.ShowOffices -> { - showProgressbar(false) - showOffices(it.offices) - } - - is CreateNewGroupUiState.ShowProgressbar -> showProgressbar(true) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + CreateNewGroupScreen( + onGroupCreated = { group -> + onGroupCreated(group) + } + ) } } - - return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.btnSubmit.setOnClickListener { - if (Network.isOnline(requireContext())) { - val groupPayload = GroupPayload() - groupPayload.name = binding.etGroupName.editableText.toString() - groupPayload.externalId = binding.etGroupExternalId.editableText.toString() - groupPayload.active = binding.cbGroupActiveStatus.isChecked - groupPayload.activationDate = activationDateString - groupPayload.submittedOnDate = dateofsubmissionstring - groupPayload.officeId = officeId!! - groupPayload.dateFormat = "dd MMMM yyyy" - groupPayload.locale = "en" - initiateGroupCreation(groupPayload) - } else { - Toaster.show(binding.root, R.string.error_network_not_available, Toaster.LONG) - } - } - - binding.cbGroupActiveStatus.setOnCheckedChangeListener { compoundButton, isChecked -> - if (isChecked) { - binding.activateDateFieldContainer.visibility = View.VISIBLE - } else { - binding.activateDateFieldContainer.visibility = View.GONE - } - } - - binding.officeListField.setOnItemClickListener { adapterView, view, relativePosition, l -> - val index = mListOffices.indexOf(adapterView.getItemAtPosition(relativePosition)) - officeId = officeList[index].id - } - } - - private fun initiateGroupCreation(groupPayload: GroupPayload) { - //TextField validations - if (!isGroupNameValid) { - return - } - viewModel.createGroup(groupPayload) - } - - private fun inflateSubmissionDate() { - binding.submittedDateFieldContainer.setEndIconOnClickListener { - submissionDatePickerDialog.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - } - - private fun inflateActivationDate() { - binding.activateDateFieldContainer.setEndIconOnClickListener { - activationDatePickerDialog.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - } - - private val isGroupNameValid: Boolean - get() { - result = true - try { - if (TextUtils.isEmpty(binding.etGroupName.editableText.toString())) { - throw RequiredFieldException( - resources.getString(R.string.group_name), - resources.getString(R.string.error_cannot_be_empty) - ) - } - if (binding.etGroupName.editableText.toString() - .trim { it <= ' ' }.length < 4 && binding.etGroupName - .editableText.toString().trim { it <= ' ' }.isNotEmpty() - ) { - throw ShortOfLengthException(resources.getString(R.string.group_name), 4) - } - if (!ValidationUtil.isNameValid(binding.etGroupName.editableText.toString())) { - throw InvalidTextInputException( - resources.getString(R.string.group_name), - resources.getString(R.string.error_should_contain_only), - InvalidTextInputException.TYPE_ALPHABETS - ) - } - } catch (e: InvalidTextInputException) { - e.notifyUserWithToast(activity) - result = false - } catch (e: ShortOfLengthException) { - e.notifyUserWithToast(activity) - result = false - } catch (e: RequiredFieldException) { - e.notifyUserWithToast(activity) - result = false - } - return result - } - - private fun showOffices(offices: List) { - officeList = offices - for (office in offices) { - office.name?.let { mListOffices.add(it) } - } - mListOffices.sort() - binding.officeListField.setSimpleItems(mListOffices.toTypedArray()) - } - - private fun showGroupCreatedSuccessfully(group: SaveResponse?) { - Toast.makeText( - activity, "Group " + MifosResponseHandler.response, - Toast.LENGTH_LONG - ).show() + private fun onGroupCreated(group: SaveResponse?) { requireActivity().supportFragmentManager.popBackStack() if (PrefManager.userStatus == Constants.USER_ONLINE) { val groupActivityIntent = Intent(activity, GroupsActivity::class.java) @@ -233,15 +43,12 @@ class CreateNewGroupFragment : ProgressableFragment() { Constants.GROUP_ID, group?.groupId ) + /** + * On group creation [InvocationTargetException] exception is thrown And app crashes + * Original XML design fragment had this bug. Not sure if it's a bug or intentional. + * I am leaving it as it is. + */ startActivity(groupActivityIntent) } } - - private fun showFetchingError(s: String?) { - Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() - } - - private fun showProgressbar(b: Boolean) { - showProgress(b) - } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt new file mode 100644 index 00000000000..c8e6df2a545 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt @@ -0,0 +1,250 @@ +/* + * This project is licensed under the open source MPL V2. + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ + +/* +package com.mifos.mifosxdroid.online.createnewgroup + +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import com.mifos.core.common.utils.Constants +import com.mifos.core.objects.group.GroupPayload +import com.mifos.core.objects.organisation.Office +import com.mifos.core.objects.response.SaveResponse +import com.mifos.exceptions.InvalidTextInputException +import com.mifos.exceptions.RequiredFieldException +import com.mifos.exceptions.ShortOfLengthException +import com.mifos.mifosxdroid.R +import com.mifos.mifosxdroid.core.ProgressableFragment +import com.mifos.mifosxdroid.core.util.Toaster +import com.mifos.mifosxdroid.databinding.FragmentCreateNewGroupBinding +import com.mifos.mifosxdroid.online.GroupsActivity +import com.mifos.utils.DatePickerConstrainType +import com.mifos.utils.FragmentConstants +import com.mifos.utils.MifosResponseHandler +import com.mifos.utils.Network +import com.mifos.utils.PrefManager +import com.mifos.utils.ValidationUtil +import com.mifos.utils.getDatePickerDialog +import com.mifos.utils.getTodayFormatted +import dagger.hilt.android.AndroidEntryPoint +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Locale + +/** + * Created by nellyk on 1/22/2016. + */ //TODO Show Image and Text after successful or Failed during creation of Group and +//TODO A button to Continue or Finish the GroupCreation. +@AndroidEntryPoint +class CreateNewGroupFragment : ProgressableFragment() { + + private lateinit var binding: FragmentCreateNewGroupBinding + + private lateinit var viewModel: CreateNewGroupViewModel + + private var activationDateString: String? = null + var officeId: Int? = 0 + var result = true + private var dateofsubmissionstring: String? = null + private val mListOffices: MutableList = ArrayList() + private var officeList: List = ArrayList() + + private var submissionDate: Instant = Instant.now() + private val submissionDatePickerDialog by lazy { + getDatePickerDialog(submissionDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { + val formattedDate = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(it) + submissionDate = Instant.ofEpochMilli(it) + binding.submittedDateFieldContainer.editText?.setText(formattedDate) + dateofsubmissionstring = binding.submittedDateFieldContainer.editText.toString() + } + } + private var activationDate: Instant = Instant.now() + private val activationDatePickerDialog by lazy { + getDatePickerDialog(activationDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { + val formattedDate = SimpleDateFormat("dd MM yyyy", Locale.getDefault()).format(it) + activationDate = Instant.ofEpochMilli(it) + binding.activateDateFieldContainer.editText?.setText(formattedDate) + activationDateString = binding.activateDateFieldContainer.editText.toString() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCreateNewGroupBinding.inflate(inflater, container, false) + viewModel = ViewModelProvider(this)[CreateNewGroupViewModel::class.java] + inflateSubmissionDate() + inflateActivationDate() + viewModel.loadOffices() + + //client active checkbox onCheckedListener + dateofsubmissionstring = getTodayFormatted() + binding.submittedDateFieldContainer.editText?.setText(getTodayFormatted()) + + activationDateString = getTodayFormatted() + binding.activateDateFieldContainer.editText?.setText(getTodayFormatted()) + + viewModel.createNewGroupUiState.observe(viewLifecycleOwner) { + when (it) { + is CreateNewGroupUiState.ShowFetchingError -> { + showProgressbar(false) + showFetchingError(it.message) + } + + is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { + showProgressbar(false) + showGroupCreatedSuccessfully(it.saveResponse) + } + + is CreateNewGroupUiState.ShowOffices -> { + showProgressbar(false) + showOffices(it.offices) + } + + is CreateNewGroupUiState.ShowProgressbar -> showProgressbar(true) + } + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.btnSubmit.setOnClickListener { + if (Network.isOnline(requireContext())) { + val groupPayload = GroupPayload() + groupPayload.name = binding.etGroupName.editableText.toString() + groupPayload.externalId = binding.etGroupExternalId.editableText.toString() + groupPayload.active = binding.cbGroupActiveStatus.isChecked + groupPayload.activationDate = activationDateString + groupPayload.submittedOnDate = dateofsubmissionstring + groupPayload.officeId = officeId!! + groupPayload.dateFormat = "dd MMMM yyyy" + groupPayload.locale = "en" + initiateGroupCreation(groupPayload) + } else { + Toaster.show(binding.root, R.string.error_network_not_available, Toaster.LONG) + } + } + + binding.cbGroupActiveStatus.setOnCheckedChangeListener { compoundButton, isChecked -> + if (isChecked) { + binding.activateDateFieldContainer.visibility = View.VISIBLE + } else { + binding.activateDateFieldContainer.visibility = View.GONE + } + } + + binding.officeListField.setOnItemClickListener { adapterView, view, relativePosition, l -> + val index = mListOffices.indexOf(adapterView.getItemAtPosition(relativePosition)) + officeId = officeList[index].id + } + } + + private fun initiateGroupCreation(groupPayload: GroupPayload) { + //TextField validations + if (!isGroupNameValid) { + return + } + viewModel.createGroup(groupPayload) + } + + private fun inflateSubmissionDate() { + binding.submittedDateFieldContainer.setEndIconOnClickListener { + submissionDatePickerDialog.show( + requireActivity().supportFragmentManager, + FragmentConstants.DFRAG_DATE_PICKER + ) + } + } + + private fun inflateActivationDate() { + binding.activateDateFieldContainer.setEndIconOnClickListener { + activationDatePickerDialog.show( + requireActivity().supportFragmentManager, + FragmentConstants.DFRAG_DATE_PICKER + ) + } + } + + private val isGroupNameValid: Boolean + get() { + result = true + try { + if (TextUtils.isEmpty(binding.etGroupName.editableText.toString())) { + throw RequiredFieldException( + resources.getString(R.string.group_name), + resources.getString(R.string.error_cannot_be_empty) + ) + } + if (binding.etGroupName.editableText.toString() + .trim { it <= ' ' }.length < 4 && binding.etGroupName + .editableText.toString().trim { it <= ' ' }.isNotEmpty() + ) { + throw ShortOfLengthException(resources.getString(R.string.group_name), 4) + } + if (!ValidationUtil.isNameValid(binding.etGroupName.editableText.toString())) { + throw InvalidTextInputException( + resources.getString(R.string.group_name), + resources.getString(R.string.error_should_contain_only), + InvalidTextInputException.TYPE_ALPHABETS + ) + } + } catch (e: InvalidTextInputException) { + e.notifyUserWithToast(activity) + result = false + } catch (e: ShortOfLengthException) { + e.notifyUserWithToast(activity) + result = false + } catch (e: RequiredFieldException) { + e.notifyUserWithToast(activity) + result = false + } + return result + } + + private fun showOffices(offices: List) { + officeList = offices + for (office in offices) { + office.name?.let { mListOffices.add(it) } + } + mListOffices.sort() + binding.officeListField.setSimpleItems(mListOffices.toTypedArray()) + } + + private fun showGroupCreatedSuccessfully(group: SaveResponse?) { + Toast.makeText( + activity, "Group " + MifosResponseHandler.response, + Toast.LENGTH_LONG + ).show() + requireActivity().supportFragmentManager.popBackStack() + if (PrefManager.userStatus == Constants.USER_ONLINE) { + val groupActivityIntent = Intent(activity, GroupsActivity::class.java) + groupActivityIntent.putExtra( + Constants.GROUP_ID, + group?.groupId + ) + startActivity(groupActivityIntent) + } + } + + private fun showFetchingError(s: String?) { + Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() + } + + private fun showProgressbar(b: Boolean) { + showProgress(b) + } +} +*/ \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt new file mode 100644 index 00000000000..8e06702e808 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt @@ -0,0 +1,428 @@ +package com.mifos.mifosxdroid.online.createnewgroup + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.isSystemInDarkTheme +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +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 com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosDatePickerTextField +import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.theme.BluePrimary +import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.objects.group.GroupPayload +import com.mifos.core.objects.organisation.Office +import com.mifos.core.objects.response.SaveResponse +import com.mifos.feature.note.NoteScreenPreviewProvider +import com.mifos.feature.note.NoteUiState +import com.mifos.mifosxdroid.R +import com.mifos.utils.MifosResponseHandler +import com.mifos.utils.Network +import java.text.SimpleDateFormat +import java.util.Locale + +/** + * Created by Pronay Sarker on 30/06/2024 (7:53 AM) + */ + +@Composable +fun CreateNewGroupScreen( + viewModel: CreateNewGroupViewModel = hiltViewModel(), + onGroupCreated: (group: SaveResponse?) -> Unit, +) { + val uiState by viewModel.createNewGroupUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + viewModel.loadOffices() + } + + CreateNewGroupScreen( + uiState = uiState, + onRetry = { viewModel.loadOffices() }, + invokeGroupCreation = { groupPayload -> + viewModel.createGroup(groupPayload) + }, + onGroupCreated = onGroupCreated + ) + +} + +@Composable +fun CreateNewGroupScreen( + uiState: CreateNewGroupUiState, + onRetry: () -> Unit, + invokeGroupCreation: (GroupPayload) -> Unit, + onGroupCreated: (group: SaveResponse?) -> Unit +) { + val context = LocalContext.current + + Box( + modifier = Modifier.fillMaxSize() + ) { + when (uiState) { + is CreateNewGroupUiState.ShowFetchingError -> { + MifosSweetError( + message = uiState.message, + onclick = { onRetry.invoke() } + ) + } + + is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { + Toast.makeText(context, "Group " + MifosResponseHandler.response, Toast.LENGTH_LONG) + .show() + onGroupCreated.invoke(uiState.saveResponse) + } + + is CreateNewGroupUiState.ShowOffices -> { + CreateNewGroupContent( + officeList = uiState.offices, + invokeGroupCreation = invokeGroupCreation + ) + } + + CreateNewGroupUiState.ShowProgressbar -> { + MifosCircularProgress() + } + } + + } + +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateNewGroupContent( + officeList: List, + invokeGroupCreation: (GroupPayload) -> Unit, +) { + var groupName by rememberSaveable { + mutableStateOf("") + } + var selectedOffice by rememberSaveable { + mutableStateOf("") + } + var externalId by rememberSaveable { + mutableStateOf("") + } + var submitDatePicker by rememberSaveable { + mutableStateOf(false) + } + var activationDatePicker by rememberSaveable { + mutableStateOf(false) + } + var isActive by rememberSaveable { + mutableStateOf(false) + } + + val context = LocalContext.current + val scrollState = rememberScrollState() + var officeId by rememberSaveable { mutableIntStateOf(0) } + var activationDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + var submittedOnDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val activateDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = activationDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + val sumittedDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = submittedOnDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + + if (activationDatePicker || submitDatePicker) { + DatePickerDialog( + onDismissRequest = { + submitDatePicker = false + activationDatePicker = false + }, + confirmButton = { + TextButton( + onClick = { + if (submitDatePicker) { + sumittedDatePickerState.selectedDateMillis?.let { + submittedOnDate = it + } + } else { + activateDatePickerState.selectedDateMillis?.let { + activationDate = it + } + } + submitDatePicker = false + activationDatePicker = false + } + ) { Text(stringResource(id = R.string.select_date)) } + }, + dismissButton = { + TextButton( + onClick = { + activationDatePicker = false + submitDatePicker = false + } + ) { Text(stringResource(id = R.string.cancel)) } + } + ) + { + DatePicker(state = if (submitDatePicker) sumittedDatePickerState else activateDatePickerState) + + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + style = MaterialTheme.typography.headlineSmall, + text = stringResource(id = R.string.create_new_group), + modifier = Modifier.padding(start = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosOutlinedTextField( + value = groupName, + onValueChange = { groupName = it }, + label = stringResource(id = R.string.name) + "*", + error = null + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosTextFieldDropdown( + value = selectedOffice, + onValueChanged = { + selectedOffice = it + }, + onOptionSelected = { index, value -> + selectedOffice = value + officeList[index].id?.let { + officeId = it + } + + }, + label = R.string.office_name_mandatory, + options = officeList.map { it.name.toString() }, + readOnly = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + submittedOnDate + ), + label = R.string.submit_date, + openDatePicker = { + submitDatePicker = true + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosOutlinedTextField( + value = externalId, + onValueChange = { externalId = it }, + label = stringResource(id = R.string.external_id), + error = null + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + modifier = Modifier.padding(start = 8.dp), + colors = CheckboxDefaults.colors( + if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + checked = isActive, + onCheckedChange = { isActive = !isActive } + ) + Text(text = stringResource(id = R.string.active)) + } + + if (isActive) { + Spacer(modifier = Modifier.height(16.dp)) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + activationDate + ), + label = R.string.activation_date, + openDatePicker = { + activationDatePicker = true + } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(44.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + onClick = { + if (validateFields(groupName, selectedOffice, context)) { + if (Network.isOnline(context)) { + val activationDateInString = if (isActive) SimpleDateFormat( + "dd MMMM yyyy", + Locale.getDefault() + ).format( + activationDate + ) else null + + val submittedOnDateInString = SimpleDateFormat( + "dd MMMM yyyy", + Locale.getDefault() + ).format( + submittedOnDate + ) + + invokeGroupCreation.invoke( + GroupPayload( + name = groupName, + externalId = externalId, + active = isActive, + activationDate = activationDateInString, + submittedOnDate = submittedOnDateInString, + officeId = officeId, + dateFormat = "dd MMMM yyyy", + locale = "en" + ) + ) + } else { + Toast.makeText( + context, + context.resources.getString(R.string.error_not_connected_internet), + Toast.LENGTH_SHORT + ).show() + } + } + }) { + Text(text = stringResource(id = R.string.submit)) + } + } +} + +fun validateFields(groupName: String, officeName: String, context: Context): Boolean { + return when { + groupName.isEmpty() -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_cannot_be_empty), + Toast.LENGTH_SHORT + ).show() + return false + } + + groupName.trim().length < 4 -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_must_be_at_least_four_characters_long), + Toast.LENGTH_SHORT + ).show() + return false + } + + groupName.contains("[^a-zA-Z ]".toRegex()) -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_should_contain_only_alphabets), + Toast.LENGTH_SHORT + ).show() + return false + } + + officeName.isEmpty() -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_office_not_selected), + Toast.LENGTH_SHORT + ).show() + return false + } + + else -> true + } +} + +class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + CreateNewGroupUiState.ShowProgressbar, + CreateNewGroupUiState.ShowOffices(listOf()), + CreateNewGroupUiState.ShowFetchingError("Failed to fetch Offices"), + CreateNewGroupUiState.ShowGroupCreatedSuccessfully(saveResponse = SaveResponse()), + ) +} + +@Composable +@Preview(showSystemUi = true) +fun PreviewCreateNewGroupScreen( + @PreviewParameter(CreateNewGroupScreenPreviewProvider::class) createNewGroupUiState: CreateNewGroupUiState +) { + CreateNewGroupScreen( + uiState = createNewGroupUiState, + onRetry = {}, + invokeGroupCreation = {}, + onGroupCreated = { _ -> + + } + ) +} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt index a8a4c688a0b..ca8da2df4f8 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt @@ -6,7 +6,10 @@ import androidx.lifecycle.ViewModel import com.mifos.core.objects.group.GroupPayload import com.mifos.core.objects.organisation.Office import com.mifos.core.objects.response.SaveResponse +import com.mifos.mifosxdroid.online.createnewclient.CreateNewClientUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -19,9 +22,8 @@ import javax.inject.Inject class CreateNewGroupViewModel @Inject constructor(private val repository: CreateNewGroupRepository) : ViewModel() { - private val _createNewGroupUiState = MutableLiveData() - - val createNewGroupUiState: LiveData + private val _createNewGroupUiState = MutableStateFlow(CreateNewGroupUiState.ShowProgressbar) + val createNewGroupUiState: StateFlow get() = _createNewGroupUiState fun loadOffices() { diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountFragment.kt index b44682fd6cc..82963924ffa 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountFragment.kt @@ -8,28 +8,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener -import android.widget.ArrayAdapter -import android.widget.Toast -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.ViewModelProvider +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import com.mifos.core.common.utils.Constants import com.mifos.core.data.LoansPayload -import com.mifos.core.objects.accounts.loan.Loans -import com.mifos.core.objects.organisation.LoanProducts -import com.mifos.core.objects.templates.loans.LoanTemplate -import com.mifos.core.objects.templates.loans.RepaymentFrequencyDaysOfWeekTypeOptions -import com.mifos.core.objects.templates.loans.RepaymentFrequencyNthDayTypeOptions +import com.mifos.core.objects.noncore.DataTable +import com.mifos.feature.loan.loan_account.LoanAccountScreen import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.core.ProgressableDialogFragment -import com.mifos.mifosxdroid.core.util.Toaster -import com.mifos.mifosxdroid.databinding.FragmentAddLoanBinding import com.mifos.mifosxdroid.online.datatablelistfragment.DataTableListFragment -import com.mifos.mifosxdroid.uihelpers.MFDatePicker -import com.mifos.mifosxdroid.uihelpers.MFDatePicker.OnDatePickListener -import com.mifos.utils.DateHelper import com.mifos.utils.FragmentConstants import dagger.hilt.android.AndroidEntryPoint @@ -40,69 +29,11 @@ import dagger.hilt.android.AndroidEntryPoint * Use this Fragment to Create and/or Update loan */ @AndroidEntryPoint -class LoanAccountFragment : ProgressableDialogFragment(), OnDatePickListener, - OnItemSelectedListener { +class LoanAccountFragment : Fragment() { - private lateinit var binding: FragmentAddLoanBinding private val arg: LoanAccountFragmentArgs by navArgs() - - private lateinit var viewModel: LoanAccountViewModel - - private var submissionDate: String? = null - private var disbursementDate: String? = null - private var hasDataTables = false - private var mfDatePicker: DialogFragment? = null - private var productId: Int? = 0 private var clientId = 0 - private var loanPurposeId: Int? = null - private var loanTermFrequency: Int? = null - private val loanTermFrequencyType = 0 - private var termFrequency: Int? = null - private var repaymentEvery: Int? = null - private var transactionProcessingStrategyId: Int? = null - private var amortizationTypeId: Int? = null - private var interestCalculationPeriodTypeId: Int? = null - private var fundId: Int? = null - private var loanOfficerId: Int? = null - private var interestTypeId: Int? = null - private var repaymentFrequencyNthDayType: Int? = null - private var repaymentFrequencyDayOfWeek: Int? = null - private var interestRatePerPeriod: Double? = null - private var linkAccountId: Int? = null - private var isDisbursebemntDate = false - private var isSubmissionDate = false - private var mLoanProducts: List = ArrayList() - private var mRepaymentFrequencyNthDayTypeOptions: List = - ArrayList() - private var mRepaymentFrequencyDaysOfWeekTypeOptions: List = - ArrayList() - private var mLoanTemplate = LoanTemplate() - private var mListLoanProducts: MutableList = ArrayList() - private var mListLoanPurposeOptions: MutableList = ArrayList() - private var mListAccountLinkingOptions: MutableList = ArrayList() - private var mListAmortizationTypeOptions: MutableList = ArrayList() - private var mListInterestCalculationPeriodTypeOptions: MutableList = ArrayList() - private var mListTransactionProcessingStrategyOptions: MutableList = ArrayList() - private var mListTermFrequencyTypeOptions: MutableList = ArrayList() - private var mListLoanTermFrequencyTypeOptions: MutableList = ArrayList() - private var mListRepaymentFrequencyNthDayTypeOptions: MutableList = ArrayList() - private var mListRepaymentFrequencyDayOfWeekTypeOptions: MutableList = ArrayList() - private var mListLoanFundOptions: MutableList = ArrayList() - private var mListLoanOfficerOptions: MutableList = ArrayList() - private var mListInterestTypeOptions: MutableList = ArrayList() - private var mLoanProductAdapter: ArrayAdapter? = null - private var mLoanPurposeOptionsAdapter: ArrayAdapter? = null - private var mAccountLinkingOptionsAdapter: ArrayAdapter? = null - private var mAmortizationTypeOptionsAdapter: ArrayAdapter? = null - private var mInterestCalculationPeriodTypeOptionsAdapter: ArrayAdapter? = null - private var mTransactionProcessingStrategyOptionsAdapter: ArrayAdapter? = null - private var mTermFrequencyTypeOptionsAdapter: ArrayAdapter? = null - private var mLoanTermFrequencyTypeAdapter: ArrayAdapter? = null - private var mRepaymentFrequencyNthDayTypeOptionsAdapter: ArrayAdapter? = null - private var mRepaymentFrequencyDayOfWeekTypeOptionsAdapter: ArrayAdapter? = null - private var mLoanFundOptionsAdapter: ArrayAdapter? = null - private var mLoanOfficerOptionsAdapter: ArrayAdapter? = null - private var mInterestTypeOptionsAdapter: ArrayAdapter? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) clientId = arg.clientId @@ -113,517 +44,164 @@ class LoanAccountFragment : ProgressableDialogFragment(), OnDatePickListener, container: ViewGroup?, savedInstanceState: Bundle? ): View { - activity?.actionBar?.setDisplayHomeAsUpEnabled(true) - binding = FragmentAddLoanBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[LoanAccountViewModel::class.java] - inflateSubmissionDate() - inflateDisbursementDate() - inflateLoansProductSpinner() - disbursementDate = binding.tvDisbursementonDate.text.toString() - submissionDate = binding.tvDisbursementonDate.text.toString() - submissionDate = DateHelper.getDateAsStringUsedForCollectionSheetPayload(submissionDate) - .replace("-", " ") - disbursementDate = DateHelper.getDateAsStringUsedForCollectionSheetPayload(disbursementDate) - .replace("-", " ") - inflateSpinners() - - viewModel.loanAccountUiState.observe(viewLifecycleOwner) { - when (it) { - is LoanAccountUiState.ShowAllLoan -> { - showProgressbar(false) - showAllLoan(it.productLoans) - } - - is LoanAccountUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is LoanAccountUiState.ShowLoanAccountCreatedSuccessfully -> { - showProgressbar(false) - showLoanAccountCreatedSuccessfully(it.loans) - } - - is LoanAccountUiState.ShowLoanAccountTemplate -> { - showProgressbar(false) - showLoanAccountTemplate(it.loanTemplate) - } - - is LoanAccountUiState.ShowMessage -> { - showProgressbar(false) - showMessage(it.message) - } - - is LoanAccountUiState.ShowProgressbar -> showProgressbar(true) - } - } - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.btnLoanSubmit.setOnClickListener { - submit() - } - - binding.tvSubmittedonDate.setOnClickListener { - setTvSubmittedOnDate() - } - - binding.tvDisbursementonDate.setOnClickListener { - setTvDisbursementOnDate() - } - } - - - private fun submit() { - val loansPayload = LoansPayload() - loansPayload.allowPartialPeriodInterestCalcualtion = binding.cbCalculateinterest - .isChecked - loansPayload.amortizationType = amortizationTypeId - loansPayload.clientId = clientId - loansPayload.dateFormat = "dd MMMM yyyy" - loansPayload.expectedDisbursementDate = disbursementDate - loansPayload.interestCalculationPeriodType = interestCalculationPeriodTypeId - loansPayload.loanType = "individual" - loansPayload.locale = "en" - loansPayload.numberOfRepayments = - binding.etNumberofrepayments.editableText.toString().toInt() - loansPayload.principal = binding.etPrincipal.editableText.toString().toDouble() - loansPayload.productId = productId - loansPayload.repaymentEvery = binding.etRepaidevery.editableText.toString().toInt() - loansPayload.submittedOnDate = submissionDate - loansPayload.loanPurposeId = loanPurposeId - loansPayload.loanTermFrequency = binding.etLoanterm.editableText.toString().toInt() - loansPayload.loanTermFrequencyType = loanTermFrequency - - //loanTermFrequencyType and repaymentFrequencyType should be the same. - loansPayload.repaymentFrequencyType = loanTermFrequency - loansPayload.repaymentFrequencyDayOfWeekType = - if (repaymentFrequencyDayOfWeek != null) repaymentFrequencyDayOfWeek else null - loansPayload.repaymentFrequencyNthDayType = - if (repaymentFrequencyNthDayType != null) repaymentFrequencyNthDayType else null - loansPayload.transactionProcessingStrategyId = transactionProcessingStrategyId - loansPayload.fundId = fundId - loansPayload.interestType = interestTypeId - loansPayload.loanOfficerId = loanOfficerId - loansPayload.linkAccountId = linkAccountId - interestRatePerPeriod = - binding.etNominalInterestRate.editableText.toString().toDouble() - loansPayload.interestRatePerPeriod = interestRatePerPeriod - if (hasDataTables) { - val fragment = DataTableListFragment.newInstance( - mLoanTemplate.dataTables, - loansPayload, Constants.CLIENT_LOAN - ) - val fragmentTransaction = requireActivity().supportFragmentManager - .beginTransaction() - fragmentTransaction.addToBackStack(FragmentConstants.DATA_TABLE_LIST) - fragmentTransaction.replace(R.id.container, fragment).commit() - } else { - initiateLoanCreation(loansPayload) - } - } - - override fun onDatePicked(date: String?) { - if (isSubmissionDate) { - binding.tvSubmittedonDate.text = date - submissionDate = date - isSubmissionDate = false - } - if (isDisbursebemntDate) { - binding.tvDisbursementonDate.text = date - disbursementDate = date - isDisbursebemntDate = false - } - } - - private fun inflateSpinners() { - - //Inflating the LoanProducts Spinner - mLoanProductAdapter = ArrayAdapter( - requireActivity(), android.R.layout.simple_spinner_item, - mListLoanProducts - ) - mLoanProductAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spLproduct.adapter = mLoanProductAdapter - binding.spLproduct.onItemSelectedListener = this - - //Inflating the LoanPurposeOptions - mLoanPurposeOptionsAdapter = ArrayAdapter( - requireActivity(), android.R.layout.simple_spinner_item, - mListLoanPurposeOptions - ) - mLoanPurposeOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spLoanPurpose.adapter = mLoanPurposeOptionsAdapter - binding.spLoanPurpose.onItemSelectedListener = this - - //Inflating Linking Options - mAccountLinkingOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListAccountLinkingOptions - ) - mAccountLinkingOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spLinkingOptions.adapter = mAccountLinkingOptionsAdapter - binding.spLinkingOptions.onItemSelectedListener = this - - //Inflating AmortizationTypeOptions Spinner - mAmortizationTypeOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListAmortizationTypeOptions - ) - mAmortizationTypeOptionsAdapter?.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ) - binding.spAmortization.adapter = mAmortizationTypeOptionsAdapter - binding.spAmortization.onItemSelectedListener = this - - //Inflating InterestCalculationPeriodTypeOptions Spinner - mInterestCalculationPeriodTypeOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListInterestCalculationPeriodTypeOptions - ) - mInterestCalculationPeriodTypeOptionsAdapter?.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ) - binding.spInterestcalculationperiod.adapter = mInterestCalculationPeriodTypeOptionsAdapter - binding.spInterestcalculationperiod.onItemSelectedListener = this - - //Inflate TransactionProcessingStrategyOptions Spinner - mTransactionProcessingStrategyOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListTransactionProcessingStrategyOptions - ) - mTransactionProcessingStrategyOptionsAdapter?.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ) - binding.spRepaymentstrategy.adapter = mTransactionProcessingStrategyOptionsAdapter - binding.spRepaymentstrategy.onItemSelectedListener = this - - //Inflate TermFrequencyTypeOptionsAdapter Spinner - mTermFrequencyTypeOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListTermFrequencyTypeOptions - ) - mTermFrequencyTypeOptionsAdapter?.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ) - binding.spPaymentPeriods.adapter = mTermFrequencyTypeOptionsAdapter - binding.spPaymentPeriods.onItemSelectedListener = this - - //Inflate LoanTerm Frequency Type adapter - mLoanTermFrequencyTypeAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListLoanTermFrequencyTypeOptions - ) - mLoanTermFrequencyTypeAdapter?.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ) - binding.spLoanTermPeriods.adapter = mLoanTermFrequencyTypeAdapter - binding.spLoanTermPeriods.onItemSelectedListener = this - - //Inflate FondOptions Spinner - mLoanFundOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListLoanFundOptions - ) - mLoanFundOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spFund.adapter = mLoanFundOptionsAdapter - binding.spFund.onItemSelectedListener = this - - //Inflating LoanOfficerOptions Spinner - mLoanOfficerOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListLoanOfficerOptions - ) - mLoanOfficerOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spLoanOfficer.adapter = mLoanOfficerOptionsAdapter - binding.spLoanOfficer.onItemSelectedListener = this - - //Inflating InterestTypeOptions Spinner - mInterestTypeOptionsAdapter = ArrayAdapter( - requireActivity(), - android.R.layout.simple_spinner_item, mListInterestTypeOptions - ) - mInterestTypeOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spInterestType.adapter = mInterestTypeOptionsAdapter - binding.spInterestType.onItemSelectedListener = this - } - - private fun inflateRepaidMonthSpinners() { - mRepaymentFrequencyNthDayTypeOptionsAdapter = ArrayAdapter( - requireActivity(), android.R.layout.simple_spinner_item, - mListRepaymentFrequencyNthDayTypeOptions - ) - mRepaymentFrequencyNthDayTypeOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spRepaymentFreqNthDay.adapter = mRepaymentFrequencyNthDayTypeOptionsAdapter - binding.spRepaymentFreqNthDay.onItemSelectedListener = this - mRepaymentFrequencyDayOfWeekTypeOptionsAdapter = ArrayAdapter( - requireActivity(), android.R.layout.simple_spinner_item, - mListRepaymentFrequencyDayOfWeekTypeOptions - ) - mRepaymentFrequencyDayOfWeekTypeOptionsAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spRepaymentFreqDayOfWeek.adapter = mRepaymentFrequencyDayOfWeekTypeOptionsAdapter - binding.spRepaymentFreqDayOfWeek.onItemSelectedListener = this - binding.spRepaymentFreqNthDay.setSelection(mListRepaymentFrequencyNthDayTypeOptions.size - 1) - binding.spRepaymentFreqDayOfWeek.setSelection( - mListRepaymentFrequencyDayOfWeekTypeOptions.size - 1 - ) - } - - private fun inflateLoansProductSpinner() { - viewModel.loadAllLoans() - } - - private fun inflateLoanPurposeSpinner() { - productId?.let { viewModel.loadLoanAccountTemplate(clientId, it) } - } - - private fun initiateLoanCreation(loansPayload: LoansPayload) { - viewModel.createLoansAccount(loansPayload) - } - - private fun inflateSubmissionDate() { - mfDatePicker = MFDatePicker.newInsance(this) - binding.tvSubmittedonDate.text = MFDatePicker.datePickedAsString - } - - private fun setTvSubmittedOnDate() { - isSubmissionDate = true - mfDatePicker?.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - - private fun inflateDisbursementDate() { - mfDatePicker = MFDatePicker.newInsance(this) - binding.tvDisbursementonDate.text = MFDatePicker.datePickedAsString - } - - private fun setTvDisbursementOnDate() { - isDisbursebemntDate = true - mfDatePicker?.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - - private fun showAllLoan(loans: List) { - mLoanProducts = loans - mListLoanProducts.clear() - for (loanProducts in mLoanProducts) { - loanProducts.name?.let { mListLoanProducts.add(it) } - } - mLoanProductAdapter?.notifyDataSetChanged() - } - - private fun showLoanAccountTemplate(loanTemplate: LoanTemplate) { - mLoanTemplate = loanTemplate - hasDataTables = mLoanTemplate.dataTables.size > 0 - mListRepaymentFrequencyNthDayTypeOptions.clear() - mRepaymentFrequencyNthDayTypeOptions = mLoanTemplate - .repaymentFrequencyNthDayTypeOptions - for (options in mRepaymentFrequencyNthDayTypeOptions) { - options.value?.let { mListRepaymentFrequencyNthDayTypeOptions.add(it) } - } - mListRepaymentFrequencyNthDayTypeOptions.add( - resources.getString(R.string.select_week_hint) - ) - mListRepaymentFrequencyDayOfWeekTypeOptions.clear() - mRepaymentFrequencyDaysOfWeekTypeOptions = mLoanTemplate - .repaymentFrequencyDaysOfWeekTypeOptions - for (options in mRepaymentFrequencyDaysOfWeekTypeOptions) { - options.value?.let { mListRepaymentFrequencyDayOfWeekTypeOptions.add(it) } - } - mListRepaymentFrequencyDayOfWeekTypeOptions.add( - resources.getString(R.string.select_day_hint) - ) - mListLoanPurposeOptions.clear() - for (loanPurposeOptions in mLoanTemplate.loanPurposeOptions) { - loanPurposeOptions.name?.let { mListLoanPurposeOptions.add(it) } - } - mLoanPurposeOptionsAdapter?.notifyDataSetChanged() - mListAccountLinkingOptions.clear() - for (options in mLoanTemplate.accountLinkingOptions) { - options.productName?.let { mListAccountLinkingOptions.add(it) } - } - mListAccountLinkingOptions.add( - resources.getString(R.string.select_linkage_account_hint) - ) - mAccountLinkingOptionsAdapter?.notifyDataSetChanged() - mListAmortizationTypeOptions.clear() - for (amortizationTypeOptions in mLoanTemplate.amortizationTypeOptions) { - amortizationTypeOptions.value?.let { mListAmortizationTypeOptions.add(it) } - } - mAmortizationTypeOptionsAdapter?.notifyDataSetChanged() - mListInterestCalculationPeriodTypeOptions.clear() - for (interestCalculationPeriodType in mLoanTemplate - .interestCalculationPeriodTypeOptions) { - interestCalculationPeriodType.value?.let { - mListInterestCalculationPeriodTypeOptions.add( - it + return ComposeView(requireActivity()).apply { + setContent { + LoanAccountScreen( + clientId = clientId, + onBackPressed = { + findNavController().popBackStack() + }, dataTable = { dataTables, loansPayload -> + dataTables(dataTables, loansPayload) + } ) } } - mInterestCalculationPeriodTypeOptionsAdapter?.notifyDataSetChanged() - mListTransactionProcessingStrategyOptions.clear() - for (transactionProcessingStrategyOptions in mLoanTemplate.transactionProcessingStrategyOptions) { - transactionProcessingStrategyOptions - .name?.let { - mListTransactionProcessingStrategyOptions.add( - it - ) - } - } - mTransactionProcessingStrategyOptionsAdapter?.notifyDataSetChanged() - mListTermFrequencyTypeOptions.clear() - for (termFrequencyTypeOptions in mLoanTemplate.termFrequencyTypeOptions) { - termFrequencyTypeOptions.value?.let { mListTermFrequencyTypeOptions.add(it) } - } - mTermFrequencyTypeOptionsAdapter?.notifyDataSetChanged() - mListLoanTermFrequencyTypeOptions.clear() - for (termFrequencyTypeOptions in mLoanTemplate.termFrequencyTypeOptions) { - termFrequencyTypeOptions.value?.let { mListLoanTermFrequencyTypeOptions.add(it) } - } - mLoanTermFrequencyTypeAdapter?.notifyDataSetChanged() - mListLoanFundOptions.clear() - for (fundOptions in mLoanTemplate.fundOptions) { - fundOptions.name?.let { mListLoanFundOptions.add(it) } - } - mLoanFundOptionsAdapter?.notifyDataSetChanged() - mListLoanOfficerOptions.clear() - for (loanOfficerOptions in mLoanTemplate.loanOfficerOptions) { - loanOfficerOptions.displayName?.let { mListLoanOfficerOptions.add(it) } - } - mLoanOfficerOptionsAdapter?.notifyDataSetChanged() - mListInterestTypeOptions.clear() - for (interestTypeOptions in mLoanTemplate.interestTypeOptions) { - interestTypeOptions.value?.let { mListInterestTypeOptions.add(it) } - } - mInterestTypeOptionsAdapter?.notifyDataSetChanged() - showDefaultValues() - } - - private fun showLoanAccountCreatedSuccessfully(loans: Loans?) { - Toast.makeText(activity, R.string.loan_creation_success, Toast.LENGTH_LONG).show() - requireActivity().supportFragmentManager.popBackStackImmediate() - } - - private fun showMessage(messageId: Int) { - Toaster.show(binding.root, messageId) - } - - private fun showFetchingError(s: String?) { - Toaster.show(binding.root, s) - } - - private fun showProgressbar(show: Boolean) { - showProgress(show) } - override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - when (parent.id) { - R.id.sp_lproduct -> { - productId = mLoanProducts[position].id - inflateLoanPurposeSpinner() - } - - R.id.sp_loan_purpose -> loanPurposeId = mLoanTemplate.loanPurposeOptions[position].id - R.id.sp_amortization -> amortizationTypeId = - mLoanTemplate.amortizationTypeOptions[position].id - - R.id.sp_interestcalculationperiod -> interestCalculationPeriodTypeId = mLoanTemplate - .interestCalculationPeriodTypeOptions[position].id - - R.id.sp_repaymentstrategy -> transactionProcessingStrategyId = mLoanTemplate - .transactionProcessingStrategyOptions[position].id - - R.id.sp_payment_periods -> { - loanTermFrequency = mLoanTemplate.termFrequencyTypeOptions[position] - .id - loanTermFrequency?.let { binding.spLoanTermPeriods.setSelection(it) } - if (loanTermFrequency == 2) { - // Show and inflate Nth day and week spinners - showHideRepaidMonthSpinners(View.VISIBLE) - inflateRepaidMonthSpinners() - } else { - showHideRepaidMonthSpinners(View.GONE) - } - } - - R.id.sp_loan_term_periods -> { - loanTermFrequency = mLoanTemplate.termFrequencyTypeOptions[position] - .id - loanTermFrequency?.let { binding.spPaymentPeriods.setSelection(it) } - if (loanTermFrequency == 2) { - // Show and inflate Nth day and week spinners - showHideRepaidMonthSpinners(View.VISIBLE) - inflateRepaidMonthSpinners() - } else { - showHideRepaidMonthSpinners(View.GONE) - } - } - - R.id.sp_repayment_freq_nth_day -> repaymentFrequencyNthDayType = - if (mListRepaymentFrequencyNthDayTypeOptions[position] - == resources.getString(R.string.select_week_hint) - ) { - null - } else { - mLoanTemplate - .repaymentFrequencyNthDayTypeOptions[position].id - } - - R.id.sp_repayment_freq_day_of_week -> repaymentFrequencyDayOfWeek = - if (mListRepaymentFrequencyDayOfWeekTypeOptions[position] - == resources.getString(R.string.select_day_hint) - ) { - null - } else { - mLoanTemplate - .repaymentFrequencyDaysOfWeekTypeOptions[position].id - } - - R.id.sp_fund -> fundId = mLoanTemplate.fundOptions[position].id - R.id.sp_loan_officer -> loanOfficerId = mLoanTemplate.loanOfficerOptions[position].id - R.id.sp_interest_type -> interestTypeId = mLoanTemplate.interestTypeOptions[position].id - R.id.sp_linking_options -> linkAccountId = if (mListAccountLinkingOptions[position] - == resources.getString(R.string.select_linkage_account_hint) - ) { - null - } else { - mLoanTemplate.accountLinkingOptions[position].id - } - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) {} - private fun showHideRepaidMonthSpinners(visibility: Int) { - binding.spRepaymentFreqNthDay.visibility = visibility - binding.spRepaymentFreqDayOfWeek.visibility = visibility - binding.tvRepaidNthfreqLabelOn.visibility = visibility - } - - private fun showDefaultValues() { - interestRatePerPeriod = mLoanTemplate.interestRatePerPeriod - loanTermFrequency = mLoanTemplate.termPeriodFrequencyType?.id - termFrequency = mLoanTemplate.termFrequency - binding.etPrincipal.setText(mLoanTemplate.principal.toString()) - binding.etNumberofrepayments.setText(mLoanTemplate.numberOfRepayments.toString()) - binding.tvNominalRateYearMonth.text = mLoanTemplate.interestRateFrequencyType?.value - binding.etNominalInterestRate.setText(mLoanTemplate.interestRatePerPeriod.toString()) - binding.etLoanterm.setText(termFrequency.toString()) - if (mLoanTemplate.repaymentEvery != null) { - repaymentEvery = mLoanTemplate.repaymentEvery - binding.etRepaidevery.setText(repaymentEvery.toString()) - } - if (mLoanTemplate.fundId != null) { - fundId = mLoanTemplate.fundId - binding.spFund.setSelection(mLoanTemplate.fundId!!) - } - binding.spLinkingOptions.setSelection(mListAccountLinkingOptions.size) - } + override fun onResume() { + super.onResume() + (requireActivity() as AppCompatActivity).supportActionBar?.hide() + } + + override fun onStop() { + super.onStop() + (requireActivity() as AppCompatActivity).supportActionBar?.show() + } + + private fun dataTables(dataTables: List, loansPayload: LoansPayload) { + val fragment = DataTableListFragment.newInstance( + dataTables, + loansPayload, Constants.CLIENT_LOAN + ) + val fragmentTransaction = requireActivity().supportFragmentManager + .beginTransaction() + fragmentTransaction.addToBackStack(FragmentConstants.DATA_TABLE_LIST) + fragmentTransaction.replace(R.id.container, fragment).commit() + } + +// private fun submit() { +// val loansPayload = LoansPayload() +// loansPayload.allowPartialPeriodInterestCalcualtion = binding.cbCalculateinterest +// .isChecked +// loansPayload.amortizationType = amortizationTypeId +// loansPayload.clientId = clientId +// loansPayload.dateFormat = "dd MMMM yyyy" +// loansPayload.expectedDisbursementDate = disbursementDate +// loansPayload.interestCalculationPeriodType = interestCalculationPeriodTypeId +// loansPayload.loanType = "individual" +// loansPayload.locale = "en" +// loansPayload.numberOfRepayments = +// binding.etNumberofrepayments.editableText.toString().toInt() +// loansPayload.principal = binding.etPrincipal.editableText.toString().toDouble() +// loansPayload.productId = productId +// loansPayload.repaymentEvery = binding.etRepaidevery.editableText.toString().toInt() +// loansPayload.submittedOnDate = submissionDate +// loansPayload.loanPurposeId = loanPurposeId +// loansPayload.loanTermFrequency = binding.etLoanterm.editableText.toString().toInt() +// loansPayload.loanTermFrequencyType = loanTermFrequency +// +// //loanTermFrequencyType and repaymentFrequencyType should be the same. +// loansPayload.repaymentFrequencyType = loanTermFrequency +// loansPayload.repaymentFrequencyDayOfWeekType = +// if (repaymentFrequencyDayOfWeek != null) repaymentFrequencyDayOfWeek else null +// loansPayload.repaymentFrequencyNthDayType = +// if (repaymentFrequencyNthDayType != null) repaymentFrequencyNthDayType else null +// loansPayload.transactionProcessingStrategyId = transactionProcessingStrategyId +// loansPayload.fundId = fundId +// loansPayload.interestType = interestTypeId +// loansPayload.loanOfficerId = loanOfficerId +// loansPayload.linkAccountId = linkAccountId +// interestRatePerPeriod = +// binding.etNominalInterestRate.editableText.toString().toDouble() +// loansPayload.interestRatePerPeriod = interestRatePerPeriod +// if (hasDataTables) { +// val fragment = DataTableListFragment.newInstance( +// mLoanTemplate.dataTables, +// loansPayload, Constants.CLIENT_LOAN +// ) +// val fragmentTransaction = requireActivity().supportFragmentManager +// .beginTransaction() +// fragmentTransaction.addToBackStack(FragmentConstants.DATA_TABLE_LIST) +// fragmentTransaction.replace(R.id.container, fragment).commit() +// } else { +// initiateLoanCreation(loansPayload) +// } +// } + + +// override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { +// when (parent.id) { +// R.id.sp_lproduct -> { +// productId = mLoanProducts[position].id +// inflateLoanPurposeSpinner() +// } +// +// R.id.sp_loan_purpose -> loanPurposeId = mLoanTemplate.loanPurposeOptions[position].id +// R.id.sp_amortization -> amortizationTypeId = +// mLoanTemplate.amortizationTypeOptions[position].id +// +// R.id.sp_interestcalculationperiod -> interestCalculationPeriodTypeId = mLoanTemplate +// .interestCalculationPeriodTypeOptions[position].id +// +// R.id.sp_repaymentstrategy -> transactionProcessingStrategyId = mLoanTemplate +// .transactionProcessingStrategyOptions[position].id +// +// R.id.sp_payment_periods -> { +// loanTermFrequency = mLoanTemplate.termFrequencyTypeOptions[position] +// .id +// loanTermFrequency?.let { binding.spLoanTermPeriods.setSelection(it) } +// if (loanTermFrequency == 2) { +// // Show and inflate Nth day and week spinners +// showHideRepaidMonthSpinners(View.VISIBLE) +// inflateRepaidMonthSpinners() +// } else { +// showHideRepaidMonthSpinners(View.GONE) +// } +// } +// +// R.id.sp_loan_term_periods -> { +// loanTermFrequency = mLoanTemplate.termFrequencyTypeOptions[position] +// .id +// loanTermFrequency?.let { binding.spPaymentPeriods.setSelection(it) } +// if (loanTermFrequency == 2) { +// // Show and inflate Nth day and week spinners +// showHideRepaidMonthSpinners(View.VISIBLE) +// inflateRepaidMonthSpinners() +// } else { +// showHideRepaidMonthSpinners(View.GONE) +// } +// } +// +// R.id.sp_repayment_freq_nth_day -> repaymentFrequencyNthDayType = +// if (mListRepaymentFrequencyNthDayTypeOptions[position] +// == resources.getString(R.string.select_week_hint) +// ) { +// null +// } else { +// mLoanTemplate +// .repaymentFrequencyNthDayTypeOptions[position].id +// } +// +// R.id.sp_repayment_freq_day_of_week -> repaymentFrequencyDayOfWeek = +// if (mListRepaymentFrequencyDayOfWeekTypeOptions[position] +// == resources.getString(R.string.select_day_hint) +// ) { +// null +// } else { +// mLoanTemplate +// .repaymentFrequencyDaysOfWeekTypeOptions[position].id +// } +// +// R.id.sp_fund -> fundId = mLoanTemplate.fundOptions[position].id +// R.id.sp_loan_officer -> loanOfficerId = mLoanTemplate.loanOfficerOptions[position].id +// R.id.sp_interest_type -> interestTypeId = mLoanTemplate.interestTypeOptions[position].id +// R.id.sp_linking_options -> linkAccountId = if (mListAccountLinkingOptions[position] +// == resources.getString(R.string.select_linkage_account_hint) +// ) { +// null +// } else { +// mLoanTemplate.accountLinkingOptions[position].id +// } +// } +// } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountUiState.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountUiState.kt deleted file mode 100644 index 884a5eb09d9..00000000000 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountUiState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.mifos.mifosxdroid.online.loanaccount - -import com.mifos.core.objects.accounts.loan.Loans -import com.mifos.core.objects.organisation.LoanProducts -import com.mifos.core.objects.templates.loans.LoanTemplate - -/** - * Created by Aditya Gupta on 08/08/23. - */ -sealed class LoanAccountUiState { - - data object ShowProgressbar : LoanAccountUiState() - - data class ShowMessage(val message: Int) : LoanAccountUiState() - - data class ShowAllLoan(val productLoans: List) : LoanAccountUiState() - - data class ShowLoanAccountTemplate(val loanTemplate: LoanTemplate) : LoanAccountUiState() - - data class ShowFetchingError(val message: String?) : LoanAccountUiState() - - data class ShowLoanAccountCreatedSuccessfully(val loans: Loans?) : LoanAccountUiState() -} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountViewModel.kt deleted file mode 100644 index d34d3d83345..00000000000 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountViewModel.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.mifos.mifosxdroid.online.loanaccount - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.mifos.core.data.LoansPayload -import com.mifos.core.objects.accounts.loan.Loans -import com.mifos.core.objects.organisation.LoanProducts -import com.mifos.core.objects.templates.loans.LoanTemplate -import com.mifos.mifosxdroid.R -import dagger.hilt.android.lifecycle.HiltViewModel -import rx.Subscriber -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import javax.inject.Inject - -/** - * Created by Aditya Gupta on 08/08/23. - */ -@HiltViewModel -class LoanAccountViewModel @Inject constructor(private val repository: LoanAccountRepository) : - ViewModel() { - - private val _loanAccountUiState = MutableLiveData() - - val loanAccountUiState: LiveData - get() = _loanAccountUiState - - fun loadAllLoans() { - _loanAccountUiState.value = LoanAccountUiState.ShowProgressbar - repository.allLoans() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber>() { - override fun onCompleted() { - } - - override fun onError(e: Throwable) { - _loanAccountUiState.value = - LoanAccountUiState.ShowMessage(R.string.failed_to_fetch_loan_products) - } - - override fun onNext(productLoanses: List) { - _loanAccountUiState.value = LoanAccountUiState.ShowAllLoan(productLoanses) - } - }) - } - - fun loadLoanAccountTemplate(clientId: Int, productId: Int) { - _loanAccountUiState.value = LoanAccountUiState.ShowProgressbar - repository.getLoansAccountTemplate(clientId, productId) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber() { - override fun onCompleted() { - } - - override fun onError(e: Throwable) { - _loanAccountUiState.value = - LoanAccountUiState.ShowMessage(R.string.failed_to_fetch_loan_template) - } - - override fun onNext(loanTemplate: LoanTemplate?) { - if (loanTemplate != null) { - _loanAccountUiState.value = - LoanAccountUiState.ShowLoanAccountTemplate(loanTemplate) - } - } - }) - } - - fun createLoansAccount(loansPayload: LoansPayload?) { - _loanAccountUiState.value = LoanAccountUiState.ShowProgressbar - repository.createLoansAccount(loansPayload) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber() { - override fun onCompleted() { - } - - override fun onError(e: Throwable) { - _loanAccountUiState.value = - LoanAccountUiState.ShowFetchingError(e.message.toString()) - } - - override fun onNext(loans: Loans?) { - _loanAccountUiState.value = - LoanAccountUiState.ShowLoanAccountCreatedSuccessfully(loans) - } - }) - } - -} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragment.kt index 199390a9663..30a137d1bbf 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragment.kt @@ -9,30 +9,33 @@ import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.mifos.core.objects.accounts.loan.LoanWithAssociations import com.mifos.core.objects.accounts.loan.RepaymentSchedule import com.mifos.mifosxdroid.R import com.mifos.mifosxdroid.adapters.LoanRepaymentScheduleAdapter +import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.core.ProgressableFragment import com.mifos.mifosxdroid.core.util.Toaster import com.mifos.mifosxdroid.databinding.FragmentLoanRepaymentScheduleBinding +import com.mifos.mifosxdroid.online.datatable.DataTableScreen +import com.mifos.mifosxdroid.online.loanrepayment.LoanRepaymentViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class LoanRepaymentScheduleFragment : ProgressableFragment() { +class LoanRepaymentScheduleFragment : MifosBaseFragment() { - private lateinit var binding: FragmentLoanRepaymentScheduleBinding private val arg: LoanRepaymentScheduleFragmentArgs by navArgs() + private val viewModel: LoanRepaymentScheduleViewModel by viewModels() - private lateinit var viewModel: LoanRepaymentScheduleViewModel - - private var loanAccountNumber = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - loanAccountNumber = arg.loanId - setHasOptionsMenu(false) + viewModel.loanId = arg.loanId } override fun onCreateView( @@ -40,66 +43,23 @@ class LoanRepaymentScheduleFragment : ProgressableFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentLoanRepaymentScheduleBinding.inflate(inflater, container, false) - setToolbarTitle(resources.getString(R.string.loan_repayment_schedule)) - viewModel = ViewModelProvider(this)[LoanRepaymentScheduleViewModel::class.java] - inflateRepaymentSchedule() - - viewModel.loanRepaymentScheduleUiState.observe(viewLifecycleOwner) { - when (it) { - is LoanRepaymentScheduleUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is LoanRepaymentScheduleUiState.ShowLoanRepaySchedule -> { - showProgressbar(false) - showLoanRepaySchedule(it.loanWithAssociations) - } - - is LoanRepaymentScheduleUiState.ShowProgressbar -> showProgressbar(true) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + LoanRepaymentScheduleScreen( + navigateBack = { findNavController().popBackStack() } + ) } } - - return binding.root - } - - override fun onPrepareOptionsMenu(menu: Menu) { - menu.clear() - super.onPrepareOptionsMenu(menu) - } - - private fun inflateRepaymentSchedule() { - viewModel.loadLoanRepaySchedule(loanAccountNumber) - } - - private fun showProgressbar(b: Boolean) { - showProgress(b) } - private fun showLoanRepaySchedule(loanWithAssociations: LoanWithAssociations) { - /* Activity is null - Fragment has been detached; no need to do anything. */ - if (activity == null) return - val listOfActualPeriods = loanWithAssociations - .repaymentSchedule - .getlistOfActualPeriods() - val loanRepaymentScheduleAdapter = - LoanRepaymentScheduleAdapter(requireActivity(), listOfActualPeriods) - binding.lvRepaymentSchedule.adapter = loanRepaymentScheduleAdapter - val totalRepaymentsCompleted = resources.getString(R.string.complete) + "" + - " : " - val totalRepaymentsOverdue = resources.getString(R.string.overdue) + " : " - val totalRepaymentsPending = resources.getString(R.string.pending) + " : " - //Implementing the Footer here - binding.flrsFooter.tvTotalPaid.text = totalRepaymentsCompleted + RepaymentSchedule - .getNumberOfRepaymentsComplete(listOfActualPeriods) - binding.flrsFooter.tvTotalOverdue.text = totalRepaymentsOverdue + RepaymentSchedule - .getNumberOfRepaymentsOverDue(listOfActualPeriods) - binding.flrsFooter.tvTotalUpcoming.text = totalRepaymentsPending + RepaymentSchedule - .getNumberOfRepaymentsPending(listOfActualPeriods) + override fun onResume() { + super.onResume() + toolbar?.visibility = View.GONE } - private fun showFetchingError(s: String?) { - Toaster.show(binding.root, s) + override fun onStop() { + super.onStop() + toolbar?.visibility = View.VISIBLE } -} \ No newline at end of file +} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragmentOld.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragmentOld.kt new file mode 100644 index 00000000000..e076708a95d --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragmentOld.kt @@ -0,0 +1,105 @@ +///* +// * This project is licensed under the open source MPL V2. +// * See https://github.com/openMF/android-client/blob/master/LICENSE.md +// */ +//package com.mifos.mifosxdroid.online.loanrepaymentschedule +// +//import android.os.Bundle +//import android.view.LayoutInflater +//import android.view.Menu +//import android.view.View +//import android.view.ViewGroup +//import androidx.lifecycle.ViewModelProvider +//import androidx.navigation.fragment.navArgs +//import com.mifos.core.objects.accounts.loan.LoanWithAssociations +//import com.mifos.core.objects.accounts.loan.RepaymentSchedule +//import com.mifos.mifosxdroid.R +//import com.mifos.mifosxdroid.adapters.LoanRepaymentScheduleAdapter +//import com.mifos.mifosxdroid.core.ProgressableFragment +//import com.mifos.mifosxdroid.core.util.Toaster +//import com.mifos.mifosxdroid.databinding.FragmentLoanRepaymentScheduleBinding +//import dagger.hilt.android.AndroidEntryPoint +// +//@AndroidEntryPoint +//class LoanRepaymentScheduleFragment : ProgressableFragment() { +// +// private lateinit var binding: FragmentLoanRepaymentScheduleBinding +// private val arg: LoanRepaymentScheduleFragmentArgs by navArgs() +// +// private lateinit var viewModel: LoanRepaymentScheduleViewModel +// +// private var loanAccountNumber = 0 +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// loanAccountNumber = arg.loanId +// setHasOptionsMenu(false) +// } +// +// override fun onCreateView( +// inflater: LayoutInflater, +// container: ViewGroup?, +// savedInstanceState: Bundle? +// ): View { +// binding = FragmentLoanRepaymentScheduleBinding.inflate(inflater, container, false) +// setToolbarTitle(resources.getString(R.string.loan_repayment_schedule)) +// viewModel = ViewModelProvider(this)[LoanRepaymentScheduleViewModel::class.java] +// inflateRepaymentSchedule() +// +// viewModel.loanRepaymentScheduleUiState.observe(viewLifecycleOwner) { +// when (it) { +// is LoanRepaymentScheduleUiState.ShowFetchingError -> { +// showProgressbar(false) +// showFetchingError(it.message) +// } +// +// is LoanRepaymentScheduleUiState.ShowLoanRepaySchedule -> { +// showProgressbar(false) +// showLoanRepaySchedule(it.loanWithAssociations) +// } +// +// is LoanRepaymentScheduleUiState.ShowProgressbar -> showProgressbar(true) +// } +// } +// +// return binding.root +// } +// +// override fun onPrepareOptionsMenu(menu: Menu) { +// menu.clear() +// super.onPrepareOptionsMenu(menu) +// } +// +// private fun inflateRepaymentSchedule() { +// viewModel.loadLoanRepaySchedule(loanAccountNumber) +// } +// +// private fun showProgressbar(b: Boolean) { +// showProgress(b) +// } +// +// private fun showLoanRepaySchedule(loanWithAssociations: LoanWithAssociations) { +// /* Activity is null - Fragment has been detached; no need to do anything. */ +// if (activity == null) return +// val listOfActualPeriods = loanWithAssociations +// .repaymentSchedule +// .getlistOfActualPeriods() +// val loanRepaymentScheduleAdapter = +// LoanRepaymentScheduleAdapter(requireActivity(), listOfActualPeriods) +// binding.lvRepaymentSchedule.adapter = loanRepaymentScheduleAdapter +// val totalRepaymentsCompleted = resources.getString(R.string.complete) + "" + +// " : " +// val totalRepaymentsOverdue = resources.getString(R.string.overdue) + " : " +// val totalRepaymentsPending = resources.getString(R.string.pending) + " : " +// //Implementing the Footer here +// binding.flrsFooter.tvTotalPaid.text = totalRepaymentsCompleted + RepaymentSchedule +// .getNumberOfRepaymentsComplete(listOfActualPeriods) +// binding.flrsFooter.tvTotalOverdue.text = totalRepaymentsOverdue + RepaymentSchedule +// .getNumberOfRepaymentsOverDue(listOfActualPeriods) +// binding.flrsFooter.tvTotalUpcoming.text = totalRepaymentsPending + RepaymentSchedule +// .getNumberOfRepaymentsPending(listOfActualPeriods) +// } +// +// private fun showFetchingError(s: String?) { +// Toaster.show(binding.root, s) +// } +//} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleScreen.kt new file mode 100644 index 00000000000..c38207c0a15 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleScreen.kt @@ -0,0 +1,357 @@ +package com.mifos.mifosxdroid.online.loanrepaymentschedule + +import androidx.compose.foundation.Canvas +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +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 com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.objects.accounts.loan.LoanWithAssociations +import com.mifos.core.objects.accounts.loan.Period +import com.mifos.core.objects.accounts.loan.RepaymentSchedule +import com.mifos.mifosxdroid.R +import com.mifos.utils.DateHelper + +/** + * Created by Pronay Sarker on 03/07/2024 (9:18 AM) + */ + +@Composable +fun LoanRepaymentScheduleScreen( + viewModel: LoanRepaymentScheduleViewModel = hiltViewModel(), + navigateBack: () -> Unit +) { + val uiState by viewModel.loanRepaymentScheduleUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + viewModel.loadLoanRepaySchedule() + } + + LoanRepaymentScheduleScreen( + uiState = uiState, + navigateBack = navigateBack, + onRetry = { viewModel.loadLoanRepaySchedule() } + ) +} + +@Composable +fun LoanRepaymentScheduleScreen( + uiState: LoanRepaymentScheduleUiState, + navigateBack: () -> Unit, + onRetry: () -> Unit +) { + val snackbarHostState = remember { + SnackbarHostState() + } + + MifosScaffold( + title = stringResource(R.string.loan_repayment_schedule), + snackbarHostState = snackbarHostState, + icon = MifosIcons.arrowBack, + onBackPressed = navigateBack + ) { + Box(modifier = Modifier.padding(it)) { + when (uiState) { + is LoanRepaymentScheduleUiState.ShowFetchingError -> { + MifosSweetError( + message = uiState.message, + onclick = onRetry + ) + } + + is LoanRepaymentScheduleUiState.ShowLoanRepaySchedule -> { + LoanRepaymentScheduleContent( + uiState.loanWithAssociations.repaymentSchedule.getlistOfActualPeriods() + ) + } + + LoanRepaymentScheduleUiState.ShowProgressbar -> { + MifosCircularProgress() + } + } + } + } +} + +@Composable +fun LoanRepaymentScheduleContent( + periods: List +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + HeaderLoanRepaymentSchedule() + + Box { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(periods) { period -> + LoanRepaymentRowItem( + color = when { + period.complete != null && period.complete!! -> { + Color.Green + } + + period.totalOverdue != null && period.totalOverdue!! > 0 -> { + Color.Red + } + + else -> Color.Blue.copy(alpha = .7f) + + }, + date = period.dueDate?.let { DateHelper.getDateAsString(it) }, + amountDue = period.totalDueForPeriod.toString(), + amountPaid = period.totalPaidForPeriod.toString() + ) + } + } + + BottomBarLoanRepaymentSchedule( + totalPaid = RepaymentSchedule.getNumberOfRepaymentsComplete(periods).toString(), + totalOverdue = RepaymentSchedule.getNumberOfRepaymentsOverDue(periods).toString(), + tvTotalUpcoming = RepaymentSchedule.getNumberOfRepaymentsPending(periods) + .toString(), + modifier = Modifier + .align(Alignment.BottomStart) + .background(color = Color.LightGray) + ) + } + } +} + + +@Composable +fun LoanRepaymentRowItem( + color: Color, + date: String?, + amountDue: String, + amountPaid: String +) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Canvas(modifier = Modifier + .size(20.dp) + .padding(2.dp), onDraw = { + drawRect( + color = color + ) + }) + + Text( + modifier = Modifier.weight(3f), + text = date ?: "", + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.End + ) + + Text( + modifier = Modifier.weight(3f), + text = amountDue, + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.End + ) + + Text( + modifier = Modifier.weight(3f), + text = amountPaid, + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.End + ) + } + + HorizontalDivider() + } +} + +@Composable +fun HeaderLoanRepaymentSchedule() { + Box( + modifier = Modifier + .background(Color.Red.copy(alpha = .5f)) + .fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(2f), + text = stringResource(id = R.string.status), + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.weight(2f), + text = stringResource(id = R.string.date), + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(3f), + text = stringResource(id = R.string.loan_amount_due), + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(3f), + text = stringResource(id = R.string.amount_paid), + style = MaterialTheme.typography.bodyLarge, + color = Color.Black, + textAlign = TextAlign.End + ) + } + } +} + +@Composable +fun BottomBarLoanRepaymentSchedule( + totalPaid: String, + totalOverdue: String, + tvTotalUpcoming: String, + modifier: Modifier +) { + Box(modifier = modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(3.4f), + text = stringResource(id = R.string.complete) + " : " + totalPaid, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.weight(3.3f), + text = stringResource(id = R.string.pending) + " : " + tvTotalUpcoming, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(3.3f), + text = stringResource(id = R.string.overdue) + " : " + totalOverdue, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.End + ) + } + } +} + +class LoanRepaymentSchedulePreviewProvider : + PreviewParameterProvider { + + val loanWithAssociations = LoanWithAssociations( + repaymentSchedule = RepaymentSchedule( + periods = listOf( + Period( + complete = true, + totalDueForPeriod = 123.232, + totalPaidForPeriod = 34343.3434, + dueDate = listOf(2024, 6, 1) + ), + Period( + complete = true, + totalDueForPeriod = 123.232, + totalPaidForPeriod = 34343.3434, + dueDate = listOf(2024, 6, 1) + ), + Period( + complete = true, + totalDueForPeriod = 123.232, + totalPaidForPeriod = 34343.3434, + dueDate = listOf(2024, 6, 1) + ), + Period( + complete = true, + totalDueForPeriod = 123.232, + totalPaidForPeriod = 34343.3434, + dueDate = listOf(2024, 6, 1) + ), + Period( + complete = true, + totalDueForPeriod = 123.232, + totalPaidForPeriod = 34343.3434, + dueDate = listOf(2024, 6, 1) + ) + ) + ) + ) + + override val values: Sequence + get() = sequenceOf( + LoanRepaymentScheduleUiState.ShowFetchingError("Error fetching loan repayment schedule"), + LoanRepaymentScheduleUiState.ShowProgressbar, + LoanRepaymentScheduleUiState.ShowLoanRepaySchedule(loanWithAssociations) + ) +} + +@Composable +@Preview(showSystemUi = true) +fun PreviewLoanRepaymentSchedule( + @PreviewParameter(LoanRepaymentSchedulePreviewProvider::class) loanRepaymentScheduleUiState: LoanRepaymentScheduleUiState +) { + LoanRepaymentScheduleScreen( + uiState = loanRepaymentScheduleUiState, + navigateBack = { }, + onRetry = {} + ) +} + + + diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleViewModel.kt index cc3d30b1945..3d85c6ed3fa 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mifos.core.objects.accounts.loan.LoanWithAssociations import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -17,12 +19,12 @@ import javax.inject.Inject class LoanRepaymentScheduleViewModel @Inject constructor(private val repository: LoanRepaymentScheduleRepository) : ViewModel() { - private val _loanRepaymentScheduleUiState = MutableLiveData() + private val _loanRepaymentScheduleUiState = MutableStateFlow(LoanRepaymentScheduleUiState.ShowProgressbar) + val loanRepaymentScheduleUiState: StateFlow get() = _loanRepaymentScheduleUiState - val loanRepaymentScheduleUiState: LiveData - get() = _loanRepaymentScheduleUiState + var loanId = 0 - fun loadLoanRepaySchedule(loanId: Int) { + fun loadLoanRepaySchedule() { _loanRepaymentScheduleUiState.value = LoanRepaymentScheduleUiState.ShowProgressbar repository.getLoanRepaySchedule(loanId) .observeOn(AndroidSchedulers.mainThread()) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragment.kt index a78f011ad3f..a921c5e983b 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragment.kt @@ -10,29 +10,32 @@ import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.viewModels +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.mifos.core.objects.accounts.loan.LoanWithAssociations import com.mifos.mifosxdroid.adapters.LoanTransactionAdapter import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.core.util.Toaster import com.mifos.mifosxdroid.databinding.FragmentLoanTransactionsBinding +import com.mifos.mifosxdroid.online.datatable.DataTableScreen +import com.mifos.mifosxdroid.online.loanrepaymentschedule.LoanRepaymentScheduleViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class LoanTransactionsFragment : MifosBaseFragment() { - private lateinit var binding: FragmentLoanTransactionsBinding private val arg: LoanTransactionsFragmentArgs by navArgs() + private val viewModel: LoanTransactionsViewModel by viewModels() - private lateinit var viewModel: LoanTransactionsViewModel - private var adapter: LoanTransactionAdapter? = null - private var loanAccountNumber = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - loanAccountNumber = arg.loanId - setHasOptionsMenu(false) + viewModel.loanId = arg.loanId } override fun onCreateView( @@ -40,57 +43,22 @@ class LoanTransactionsFragment : MifosBaseFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentLoanTransactionsBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[LoanTransactionsViewModel::class.java] - inflateLoanTransactions() - - viewModel.loanTransactionsUiState.observe(viewLifecycleOwner) { - when (it) { - is LoanTransactionsUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is LoanTransactionsUiState.ShowLoanTransaction -> { - showProgressbar(false) - showLoanTransaction(it.loanWithAssociations) - } - - is LoanTransactionsUiState.ShowProgressBar -> showProgressbar(true) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + LoanTransactionsScreen( + navigateBack = { findNavController().popBackStack() }) } } - - return binding.root - } - - override fun onPrepareOptionsMenu(menu: Menu) { - menu.clear() - super.onPrepareOptionsMenu(menu) - } - - private fun inflateLoanTransactions() { - viewModel.loadLoanTransaction(loanAccountNumber) - } - - private fun showProgressbar(b: Boolean) { - if (b) { - showMifosProgressDialog() - } else { - hideMifosProgressDialog() - } } - private fun showLoanTransaction(loanWithAssociations: LoanWithAssociations) { - Log.i("Transaction List Size", "" + loanWithAssociations.transactions.size) - adapter = LoanTransactionAdapter( - requireActivity(), - loanWithAssociations.transactions - ) - binding.elvLoanTransactions.setAdapter(adapter) - binding.elvLoanTransactions.setGroupIndicator(null) + override fun onResume() { + super.onResume() + toolbar?.visibility = View.GONE } - private fun showFetchingError(s: String?) { - Toaster.show(binding.root, s) + override fun onStop() { + super.onStop() + toolbar?.visibility = View.VISIBLE } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragmentOld.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragmentOld.kt new file mode 100644 index 00000000000..c8a4dcb001f --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragmentOld.kt @@ -0,0 +1,96 @@ +///* +// * This project is licensed under the open source MPL V2. +// * See https://github.com/openMF/android-client/blob/master/LICENSE.md +// */ +//package com.mifos.mifosxdroid.online.loantransactions +// +//import android.os.Bundle +//import android.util.Log +//import android.view.LayoutInflater +//import android.view.Menu +//import android.view.View +//import android.view.ViewGroup +//import androidx.lifecycle.ViewModelProvider +//import androidx.navigation.fragment.navArgs +//import com.mifos.core.objects.accounts.loan.LoanWithAssociations +//import com.mifos.mifosxdroid.adapters.LoanTransactionAdapter +//import com.mifos.mifosxdroid.core.MifosBaseFragment +//import com.mifos.mifosxdroid.core.util.Toaster +//import com.mifos.mifosxdroid.databinding.FragmentLoanTransactionsBinding +//import dagger.hilt.android.AndroidEntryPoint +// +//@AndroidEntryPoint +//class LoanTransactionsFragment : MifosBaseFragment() { +// +// private lateinit var binding: FragmentLoanTransactionsBinding +// private val arg: LoanTransactionsFragmentArgs by navArgs() +// +// private lateinit var viewModel: LoanTransactionsViewModel +// +// private var adapter: LoanTransactionAdapter? = null +// private var loanAccountNumber = 0 +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// loanAccountNumber = arg.loanId +// setHasOptionsMenu(false) +// } +// +// override fun onCreateView( +// inflater: LayoutInflater, +// container: ViewGroup?, +// savedInstanceState: Bundle? +// ): View { +// binding = FragmentLoanTransactionsBinding.inflate(inflater, container, false) +// viewModel = ViewModelProvider(this)[LoanTransactionsViewModel::class.java] +// inflateLoanTransactions() +// +// viewModel.loanTransactionsUiState.observe(viewLifecycleOwner) { +// when (it) { +// is LoanTransactionsUiState.ShowFetchingError -> { +// showProgressbar(false) +// showFetchingError(it.message) +// } +// +// is LoanTransactionsUiState.ShowLoanTransaction -> { +// showProgressbar(false) +// showLoanTransaction(it.loanWithAssociations) +// } +// +// is LoanTransactionsUiState.ShowProgressBar -> showProgressbar(true) +// } +// } +// +// return binding.root +// } +// +// override fun onPrepareOptionsMenu(menu: Menu) { +// menu.clear() +// super.onPrepareOptionsMenu(menu) +// } +// +// private fun inflateLoanTransactions() { +// viewModel.loadLoanTransaction(loanAccountNumber) +// } +// +// private fun showProgressbar(b: Boolean) { +// if (b) { +// showMifosProgressDialog() +// } else { +// hideMifosProgressDialog() +// } +// } +// +// private fun showLoanTransaction(loanWithAssociations: LoanWithAssociations) { +// Log.i("Transaction List Size", "" + loanWithAssociations.transactions.size) +// adapter = LoanTransactionAdapter( +// requireActivity(), +// loanWithAssociations.transactions +// ) +// binding.elvLoanTransactions.setAdapter(adapter) +// binding.elvLoanTransactions.setGroupIndicator(null) +// } +// +// private fun showFetchingError(s: String?) { +// Toaster.show(binding.root, s) +// } +//} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsScreen.kt new file mode 100644 index 00000000000..5d1c0151c88 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsScreen.kt @@ -0,0 +1,438 @@ +package com.mifos.mifosxdroid.online.loantransactions + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.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.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.Black +import com.mifos.core.designsystem.theme.White +import com.mifos.core.objects.accounts.loan.LoanWithAssociations +import com.mifos.core.objects.accounts.loan.Transaction +import com.mifos.core.objects.accounts.loan.Type +import com.mifos.core.ui.components.MifosEmptyUi +import com.mifos.mifosxdroid.R +import com.mifos.utils.DateHelper + +/** + * Created by Pronay Sarker on 04/07/2024 (11:31 AM) + */ + +@Composable +fun LoanTransactionsScreen( + navigateBack: () -> Unit +) { + val viewModel: LoanTransactionsViewModel = hiltViewModel() + val uiState by viewModel.loanTransactionsUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + viewModel.loadLoanTransaction() + } + + LoanTransactionsScreen( + uiState = uiState, + navigateBack = navigateBack, + onRetry = { viewModel.loadLoanTransaction() } + ) +} + +@Composable +fun LoanTransactionsScreen( + uiState: LoanTransactionsUiState, + navigateBack: () -> Unit, + onRetry: () -> Unit +) { + val snackbarHostState = remember { + SnackbarHostState() + } + MifosScaffold( + snackbarHostState = snackbarHostState, + title = stringResource(id = R.string.loan_transactions), + icon = MifosIcons.arrowBack, + onBackPressed = navigateBack + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + when (uiState) { + is LoanTransactionsUiState.ShowFetchingError -> { + MifosSweetError( + message = uiState.message, + onclick = onRetry + ) + } + + is LoanTransactionsUiState.ShowLoanTransaction -> { + if (uiState.loanWithAssociations.transactions.isEmpty()) { + MifosEmptyUi(text = stringResource(id = R.string.no_transactions)) + } else LoanTransactionsContent(uiState.loanWithAssociations.transactions) + + } + + LoanTransactionsUiState.ShowProgressBar -> { + MifosCircularProgress() + } + } + } + } +} + +@Composable +fun LoanTransactionsContent( + transactions: List +) { + LazyColumn { + items(transactions) { transaction -> + LoanTransactionsItemRow(transaction = transaction) + } + } +} + +@Composable +fun LoanTransactionsItemRow(transaction: Transaction) { + + val density = LocalDensity.current + var showDetails by rememberSaveable { + mutableStateOf(false) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(0.dp), + onClick = { showDetails = !showDetails }, + colors = CardDefaults.cardColors( + containerColor = White + ) + ) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 10.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (!showDetails) MifosIcons.arrowDown else MifosIcons.arrowUp, + contentDescription = "" + ) + + Text( + modifier = Modifier + .weight(3f) + .padding(start = 8.dp), + text = DateHelper.getDateAsString(transaction.date), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier + .weight(3.3f) + .padding(start = 8.dp), + text = transaction.type?.value.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier + .weight(2.7f) + .padding(start = 8.dp), + text = transaction.amount.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.End + ) + } + } + } + + AnimatedVisibility( + visible = showDetails, + enter = slideInVertically { + with(density) { -40.dp.roundToPx() } + } + expandVertically( + expandFrom = Alignment.Top + ) + fadeIn( + initialAlpha = 0.3f + ), + exit = slideOutVertically() + shrinkVertically() + fadeOut() + ) { + LoanTransactionsItemDetailsCard(transaction = transaction) + } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) +} + +@Composable +fun LoanTransactionsItemDetailsCard( + transaction: Transaction +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFe7eb9a) + ), + elevation = CardDefaults.cardElevation(2.dp), + shape = RoundedCornerShape(0.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.id), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = transaction.id.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.office), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = transaction.officeName.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 8.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(id = R.string.break_down), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + } + + HorizontalDivider(color = Black) + + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + .background(Color(0xFFdea164)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + Text( + modifier = Modifier.weight(2.5f), + text = stringResource(id = R.string.principal), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.weight(2.5f), + text = stringResource(id = R.string.loan_interest), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(2.5f), + text = stringResource(id = R.string.loan_fees), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(2.5f), + text = stringResource(id = R.string.loan_penalty), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.End + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + Text( + modifier = Modifier.weight(2.5f), + text = transaction.principalPortion.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier + .weight(2.5f) + .padding(horizontal = 4.dp), + text = transaction.interestPortion.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier.weight(2.5f), + text = transaction.feeChargesPortion.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Text( + modifier = Modifier + .weight(2.5f) + .padding(start = 4.dp), + text = transaction.penaltyChargesPortion.toString(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.End + ) + } + } + } +} + +class LoanTransactionsPreviewProvider : PreviewParameterProvider { + val transaction = + Transaction( + id = 23, + officeName = "Main office", + date = listOf(2024, 6, 1), + principalPortion = 121.2, + penaltyChargesPortion = 32323.232, + overpaymentPortion = 23232.23, + feeChargesPortion = 323.3, + interestPortion = 232.3, + type = Type( + value = "Repayment" + ) + ) + + override val values: Sequence + get() = sequenceOf( + LoanTransactionsUiState.ShowFetchingError(""), + LoanTransactionsUiState.ShowProgressBar, + LoanTransactionsUiState.ShowLoanTransaction( + LoanWithAssociations( + transactions = listOf( + transaction, + transaction, + transaction, + transaction, + transaction, + transaction, + transaction, + transaction, + transaction, + transaction, + ) + ) + ) + ) +} + +@Composable +@Preview(showSystemUi = true) +fun PreviewLoanTransactions( + @PreviewParameter(LoanTransactionsPreviewProvider::class) loanTransactionsUiState: LoanTransactionsUiState +) { + LoanTransactionsScreen( + uiState = loanTransactionsUiState, + navigateBack = {}, + onRetry = {} + ) +} + diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsViewModel.kt index 78527adaa14..065b441075f 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsViewModel.kt @@ -1,10 +1,10 @@ package com.mifos.mifosxdroid.online.loantransactions -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mifos.core.objects.accounts.loan.LoanWithAssociations import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -17,14 +17,14 @@ import javax.inject.Inject class LoanTransactionsViewModel @Inject constructor(private val repository: LoanTransactionsRepository) : ViewModel() { - private val _loanTransactionsUiState = MutableLiveData() + private val _loanTransactionsUiState = MutableStateFlow(LoanTransactionsUiState.ShowProgressBar) + val loanTransactionsUiState: StateFlow get() = _loanTransactionsUiState - val loanTransactionsUiState: LiveData - get() = _loanTransactionsUiState + var loanId = 0 - fun loadLoanTransaction(loan: Int) { + fun loadLoanTransaction() { _loanTransactionsUiState.value = LoanTransactionsUiState.ShowProgressBar - repository.getLoanTransactions(loan) + repository.getLoanTransactions(loanId) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(object : Subscriber() { diff --git a/mifosng-android/src/main/res/layout/fragment_create_new_group.xml b/mifosng-android/src/main/res/layout/fragment_create_new_group.xml index 6c4ab2ed95b..7ebe3c84e6a 100755 --- a/mifosng-android/src/main/res/layout/fragment_create_new_group.xml +++ b/mifosng-android/src/main/res/layout/fragment_create_new_group.xml @@ -8,11 +8,11 @@ android:padding="10dp"> - + + + + + - - + - - + Loan Terms Create Center Create Group + Create New Group Create Identifier Staff Id Office Id ID Charge Id Transactions + Loan Transactions Name Charge Name Description @@ -460,6 +462,7 @@ Add Charges + Office not selected Invalid URL Invalid connection Data Invalid username length @@ -480,6 +483,9 @@ Location Unavailable, Please try again later! Client List Group List + Group name can not be empty + Group name should contain only alphabets + Group name must be at least 4 characters long Invalid Amount Entered Please Select an Office Please Select a Group @@ -820,6 +826,7 @@ Checker Inbox Select From Date Select To Date + Select Date APPROVE Do you want to approve this entry? Yes diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a4f617b6b2..34ba8e036aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,3 +45,4 @@ include(":feature:report") include(":feature:path-tracking") include(":feature:note") include(":feature:activate") +include(":feature:loan")