From 2415da5b7067c7b12e1ee65276317362d287d8a4 Mon Sep 17 00:00:00 2001 From: Pronay Sarker Date: Fri, 5 Jul 2024 23:22:56 +0600 Subject: [PATCH 1/5] MIFOSAC-199 migrate loan transactions to compose (#2128) --- .../LoanTransactionsFragment.kt | 72 +-- .../LoanTransactionsFragmentOld.kt | 96 ++++ .../LoanTransactionsScreen.kt | 438 ++++++++++++++++++ .../LoanTransactionsViewModel.kt | 14 +- .../src/main/res/values/strings.xml | 1 + 5 files changed, 562 insertions(+), 59 deletions(-) create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsFragmentOld.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loantransactions/LoanTransactionsScreen.kt 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/values/strings.xml b/mifosng-android/src/main/res/values/strings.xml index 4ccaec99563..54895f8e7f0 100755 --- a/mifosng-android/src/main/res/values/strings.xml +++ b/mifosng-android/src/main/res/values/strings.xml @@ -68,6 +68,7 @@ ID Charge Id Transactions + Loan Transactions Name Charge Name Description From c95b401f59bcb121f6cef3358ad01988bf16343b Mon Sep 17 00:00:00 2001 From: Pronay Sarker Date: Fri, 5 Jul 2024 23:23:31 +0600 Subject: [PATCH 2/5] MIFOSAC-197 migrate loan repayment fragment to compose (#2127) --- .../LoanRepaymentScheduleFragment.kt | 86 ++--- .../LoanRepaymentScheduleFragmentOld.kt | 105 ++++++ .../LoanRepaymentScheduleScreen.kt | 357 ++++++++++++++++++ .../LoanRepaymentScheduleViewModel.kt | 10 +- 4 files changed, 491 insertions(+), 67 deletions(-) create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleFragmentOld.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanrepaymentschedule/LoanRepaymentScheduleScreen.kt 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()) From 3f9f33d3e64f9dd98cbb0b9d5d07d38110eb1055 Mon Sep 17 00:00:00 2001 From: Pronay Sarker Date: Sun, 7 Jul 2024 00:01:45 +0600 Subject: [PATCH 3/5] MIFOSAC-174 Migrate Create New Group Fragment to compose (#2122) createNewGroup migration createNewGroup migration migrate createNewGroup to compose change string value edit comment --- .../component/MifosEditTextField.kt | 59 +++ .../createnewgroup/CreateNewGroupFragment.kt | 233 +--------- .../CreateNewGroupFragmentOld.kt | 250 ++++++++++ .../createnewgroup/CreateNewGroupScreen.kt | 428 ++++++++++++++++++ .../createnewgroup/CreateNewGroupViewModel.kt | 8 +- .../res/layout/fragment_create_new_group.xml | 10 +- .../src/main/res/values/strings.xml | 6 + 7 files changed, 773 insertions(+), 221 deletions(-) create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt 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..b1002141ede 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,6 +102,61 @@ 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( 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/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 @@ -461,6 +462,7 @@ Add Charges + Office not selected Invalid URL Invalid connection Data Invalid username length @@ -481,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 @@ -821,6 +826,7 @@ Checker Inbox Select From Date Select To Date + Select Date APPROVE Do you want to approve this entry? Yes From fb969f6280c11d70fa01e4866d244a9ead976542 Mon Sep 17 00:00:00 2001 From: Aditya Gupta <94394661+Aditya-gupta99@users.noreply.github.com> Date: Sat, 6 Jul 2024 23:32:39 +0530 Subject: [PATCH 4/5] refactor: refactor add loan account fragment to compose (#2126) --- .../java/com/mifos/core/data/di/DataModule.kt | 11 +- .../data/repository}/LoanAccountRepository.kt | 10 +- .../LoanAccountRepositoryImp.kt | 12 +- .../component/MifosEditTextField.kt | 62 +- .../component/MifosTextFieldDropdown.kt | 8 +- .../use_cases/CreateLoanAccountUseCase.kt | 41 + .../domain/use_cases/GetAllLoanUseCase.kt | 38 + .../GetLoansAccountTemplateUseCase.kt | 41 + feature/loan/.gitignore | 1 + feature/loan/build.gradle.kts | 23 + feature/loan/consumer-rules.pro | 0 feature/loan/proguard-rules.pro | 21 + .../feature/loan/ExampleInstrumentedTest.kt | 24 + feature/loan/src/main/AndroidManifest.xml | 4 + .../loan/loan_account/LoanAccountScreen.kt | 644 +++++++++++++++ .../loan/loan_account/LoanAccountUiState.kt | 19 + .../loan/loan_account/LoanAccountViewModel.kt | 75 ++ feature/loan/src/main/res/values/strings.xml | 37 + .../com/mifos/feature/loan/ExampleUnitTest.kt | 17 + mifosng-android/build.gradle.kts | 1 + .../injection/module/RepositoryModule.kt | 7 - .../online/loanaccount/LoanAccountFragment.kt | 748 ++++-------------- .../online/loanaccount/LoanAccountUiState.kt | 23 - .../loanaccount/LoanAccountViewModel.kt | 93 --- .../src/main/res/navigation/nav_graph.xml | 8 +- settings.gradle.kts | 1 + 26 files changed, 1237 insertions(+), 732 deletions(-) rename {mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount => core/data/src/main/java/com/mifos/core/data/repository}/LoanAccountRepository.kt (53%) rename {mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount => core/data/src/main/java/com/mifos/core/data/repository_imp}/LoanAccountRepositoryImp.kt (63%) create mode 100644 core/domain/src/main/java/com/mifos/core/domain/use_cases/CreateLoanAccountUseCase.kt create mode 100644 core/domain/src/main/java/com/mifos/core/domain/use_cases/GetAllLoanUseCase.kt create mode 100644 core/domain/src/main/java/com/mifos/core/domain/use_cases/GetLoansAccountTemplateUseCase.kt create mode 100644 feature/loan/.gitignore create mode 100644 feature/loan/build.gradle.kts create mode 100644 feature/loan/consumer-rules.pro create mode 100644 feature/loan/proguard-rules.pro create mode 100644 feature/loan/src/androidTest/java/com/mifos/feature/loan/ExampleInstrumentedTest.kt create mode 100644 feature/loan/src/main/AndroidManifest.xml create mode 100644 feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountScreen.kt create mode 100644 feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountUiState.kt create mode 100644 feature/loan/src/main/java/com/mifos/feature/loan/loan_account/LoanAccountViewModel.kt create mode 100644 feature/loan/src/main/res/values/strings.xml create mode 100644 feature/loan/src/test/java/com/mifos/feature/loan/ExampleUnitTest.kt delete mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountUiState.kt delete mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccount/LoanAccountViewModel.kt 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 b35813f0eb5..120dca3e71a 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 @@ -5,11 +5,12 @@ import com.mifos.core.data.repository.CenterDetailsRepository import com.mifos.core.data.repository.CenterListRepository import com.mifos.core.data.repository.CheckerInboxRepository import com.mifos.core.data.repository.CheckerInboxTasksRepository -import com.mifos.core.data.repository.ClientIdentifiersRepository import com.mifos.core.data.repository.ClientChargeRepository +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 @@ -17,11 +18,12 @@ import com.mifos.core.data.repository_imp.CenterDetailsRepositoryImp import com.mifos.core.data.repository_imp.CenterListRepositoryImp import com.mifos.core.data.repository_imp.CheckerInboxRepositoryImp import com.mifos.core.data.repository_imp.CheckerInboxTasksRepositoryImp -import com.mifos.core.data.repository_imp.ClientIdentifiersRepositoryImp import com.mifos.core.data.repository_imp.ClientChargeRepositoryImp +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 @@ -65,10 +67,13 @@ abstract class DataModule { @Binds internal abstract fun bindClientChargeRepository(impl: ClientChargeRepositoryImp): ClientChargeRepository - + @Binds internal abstract fun bindCreateNewCenterRepository(impl: CreateNewCenterRepositoryImp): CreateNewCenterRepository @Binds internal abstract fun bindClientIdentifiersRepository(impl: ClientIdentifiersRepositoryImp): ClientIdentifiersRepository + + @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 b1002141ede..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 @@ -163,7 +163,7 @@ fun MifosOutlinedTextField( modifier: Modifier = Modifier, value: String, label: String, - leadingIcon: ImageVector, + leadingIcon: ImageVector? = null, maxLines: Int = 1, isError: Boolean = false, errorText: String? = null, @@ -205,7 +205,7 @@ fun MifosOutlinedTextField( ) }, leadingIcon = { - Icon(imageVector = leadingIcon, contentDescription = "leadingIcon") + leadingIcon?.let { Icon(imageVector = it, contentDescription = "leadingIcon") } }, trailingIcon = @Composable { if (isPasswordToggleDisplayed) { @@ -261,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, @@ -309,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 f782405949a..090da89d10a 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -138,6 +138,7 @@ dependencies { implementation(projects.feature.about) implementation(projects.feature.report) implementation(projects.feature.pathTracking) + 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/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/res/navigation/nav_graph.xml b/mifosng-android/src/main/res/navigation/nav_graph.xml index 0947c638673..edc3d5bca44 100644 --- a/mifosng-android/src/main/res/navigation/nav_graph.xml +++ b/mifosng-android/src/main/res/navigation/nav_graph.xml @@ -207,14 +207,14 @@ - - + - - + Date: Sun, 7 Jul 2024 22:46:17 +0600 Subject: [PATCH 5/5] refactor: Migrate LoanAccountSummary fragment to compose (#2124) * MIFOSAC-184 Migrate loan account summary to compose backUp backup backup backup * fix: build fail (#2118) * refactor: refactor About activity to compose (#2115) * refactor: refactor About activity to compose * fix : added text style * refactor: refactor Create new center fragment to compose (#2116) * refactor: refactor Create new center fragment to compose * fix: fix build fail * refactor: refactor client charge fragment to compose (#2120) * Migrate Data Table Fragment to compose (#2119) migrate dataTable to compose topbar fix removed extra file fix topbar made required changes * add scrollState * removed unused function remove extra icon added horizontal padding 24.dp fix scale Fix scale --------- Co-authored-by: Aditya Gupta <94394661+Aditya-gupta99@users.noreply.github.com> --- .../LoanAccountSummaryFragment.kt | 343 ++-------- .../LoanAccountSummaryFragmentOld.kt | 370 +++++++++++ .../LoanAccountSummaryScreen.kt | 616 ++++++++++++++++++ .../LoanAccountSummaryUiState.kt | 3 +- .../LoanAccountSummaryViewModel.kt | 9 +- .../layout/fragment_loan_account_summary.xml | 10 +- .../src/main/res/values/strings.xml | 3 + 7 files changed, 1060 insertions(+), 294 deletions(-) create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragmentOld.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryScreen.kt diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragment.kt index 7ab565976f5..99a73993bd2 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragment.kt @@ -5,23 +5,24 @@ package com.mifos.mifosxdroid.online.loanaccountsummary import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.mifos.core.common.utils.Constants import com.mifos.core.objects.accounts.loan.LoanWithAssociations import com.mifos.core.objects.client.Charges import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.core.ProgressableFragment +import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.databinding.FragmentLoanAccountSummaryBinding +import com.mifos.mifosxdroid.online.savingaccountsummary.SavingsAccountSummaryFragment.Companion.MENU_ITEM_DATA_TABLES +import com.mifos.mifosxdroid.online.savingaccountsummary.SavingsAccountSummaryFragment.Companion.MENU_ITEM_DOCUMENTS import com.mifos.utils.DateHelper import dagger.hilt.android.AndroidEntryPoint @@ -29,185 +30,61 @@ import dagger.hilt.android.AndroidEntryPoint * Created by ishankhanna on 09/05/14. */ @AndroidEntryPoint -class LoanAccountSummaryFragment : ProgressableFragment() { - - private lateinit var binding: FragmentLoanAccountSummaryBinding - private val arg: LoanAccountSummaryFragmentArgs by navArgs() +class LoanAccountSummaryFragment : MifosBaseFragment() { var loanAccountNumber = 0 - - private lateinit var viewModel: LoanAccountSummaryViewModel - + private val arg: LoanAccountSummaryFragmentArgs by navArgs() var chargesList: MutableList = ArrayList() - // Action Identifier in the onProcessTransactionClicked Method - private var processLoanTransactionAction = -1 - private var parentFragment = true - private var clientLoanWithAssociations: LoanWithAssociations? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loanAccountNumber = arg.loanAccountNumber - parentFragment = arg.parentFragment - //Necessary Call to add and update the Menu in a Fragment - setHasOptionsMenu(true) } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentLoanAccountSummaryBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[LoanAccountSummaryViewModel::class.java] - inflateLoanAccountSummary() - - viewModel.loanAccountSummaryUiState.observe(viewLifecycleOwner) { - when (it) { - is LoanAccountSummaryUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is LoanAccountSummaryUiState.ShowLoanById -> { - showProgressbar(false) - showLoanById(it.loanWithAssociations) - } - - is LoanAccountSummaryUiState.ShowProgressbar -> showProgressbar(true) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + LoanAccountSummaryScreen( + loanAccountNumber = loanAccountNumber, + navigateBack = { findNavController().popBackStack() }, + onTransactionsClicked = { loadLoanTransactions(it) }, + onDocumentsClicked = { loadDocuments() }, + onChargesClicked = { loadLoanCharges() }, + onRepaymentScheduleClicked = { loadRepaymentSchedule(it) }, + onMoreInfoClicked = { loadLoanDataTables() }, + approveLoan = { approveLoan(it) }, + disburseLoan = { disburseLoan() }, + onRepaymentClick = { makeRepayment(it) } + ) } } - - return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.btProcessLoanTransaction.setOnClickListener { - onProcessTransactionClicked() - } - } - private fun inflateLoanAccountSummary() { - showProgress(true) - setToolbarTitle(resources.getString(R.string.loanAccountSummary)) - //TODO Implement cases to enable/disable repayment button - binding.btProcessLoanTransaction.isEnabled = false - viewModel.loadLoanById(loanAccountNumber) - } - - private fun onProcessTransactionClicked() { - when (processLoanTransactionAction) { - TRANSACTION_REPAYMENT -> { - makeRepayment(clientLoanWithAssociations) - } - - ACTION_APPROVE_LOAN -> { - approveLoan() - } - - ACTION_DISBURSE_LOAN -> { - disburseLoan() - } - - else -> { - Log.i(requireActivity().localClassName, "TRANSACTION ACTION NOT SET") - } - } - } - - override fun onDetach() { - super.onDetach() - if (!parentFragment) { - requireActivity().finish() - } - } - - override fun onPrepareOptionsMenu(menu: Menu) { - menu.clear() - menu.add(Menu.NONE, MENU_ITEM_DATA_TABLES, Menu.NONE, Constants.DATA_TABLE_LOAN_NAME) - menu.add( - Menu.NONE, - MENU_ITEM_LOAN_TRANSACTIONS, - Menu.NONE, - resources.getString(R.string.transactions) - ) - menu.add( - Menu.NONE, - MENU_ITEM_REPAYMENT_SCHEDULE, - Menu.NONE, - resources.getString(R.string.loan_repayment_schedule) - ) - menu.add(Menu.NONE, MENU_ITEM_DOCUMENTS, Menu.NONE, resources.getString(R.string.documents)) - menu.add(Menu.NONE, MENU_ITEM_CHARGES, Menu.NONE, resources.getString(R.string.charges)) - super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - when (id) { - MENU_ITEM_REPAYMENT_SCHEDULE -> loadRepaymentSchedule(loanAccountNumber) - MENU_ITEM_LOAN_TRANSACTIONS -> loadLoanTransactions(loanAccountNumber) - MENU_ITEM_DOCUMENTS -> loadDocuments() - MENU_ITEM_CHARGES -> loadLoanCharges() - MENU_ITEM_DATA_TABLES -> loadLoanDataTables() - else -> { - } - } - return super.onOptionsItemSelected(item) + private fun loadRepaymentSchedule(loanId: Int) { + val action = + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentScheduleFragment( + loanId + ) + findNavController().navigate(action) } - private fun inflateLoanSummary(loanWithAssociations: LoanWithAssociations) { - binding.tvAmountDisbursed.text = loanWithAssociations.summary - .principalDisbursed.toString() - try { - binding.tvDisbursementDate.text = loanWithAssociations - .timeline.actualDisbursementDate?.let { - DateHelper.getDateAsString( - it as List - ) - } - } catch (exception: IndexOutOfBoundsException) { - Toast.makeText( - activity, - resources.getString(R.string.loan_rejected_message), - Toast.LENGTH_SHORT - ).show() - } - binding.tvInArrears.text = loanWithAssociations.summary.totalOverdue.toString() - binding.tvPrincipal.text = loanWithAssociations.summary - .principalDisbursed.toString() - binding.tvLoanPrincipalDue.text = loanWithAssociations.summary - .principalOutstanding.toString() - binding.tvLoanPrincipalPaid.text = loanWithAssociations.summary - .principalPaid.toString() - binding.tvInterest.text = loanWithAssociations.summary.interestCharged.toString() - binding.tvLoanInterestDue.text = loanWithAssociations.summary - .interestOutstanding.toString() - binding.tvLoanInterestPaid.text = loanWithAssociations.summary - .interestPaid.toString() - binding.tvFees.text = loanWithAssociations.summary.feeChargesCharged.toString() - binding.tvLoanFeesDue.text = loanWithAssociations.summary - .feeChargesOutstanding.toString() - binding.tvLoanFeesPaid.text = loanWithAssociations.summary - .feeChargesPaid.toString() - binding.tvPenalty.text = loanWithAssociations.summary - .penaltyChargesCharged.toString() - binding.tvLoanPenaltyDue.text = loanWithAssociations.summary - .penaltyChargesOutstanding.toString() - binding.tvLoanPenaltyPaid.text = loanWithAssociations.summary - .penaltyChargesPaid.toString() - binding.tvTotal.text = loanWithAssociations.summary - .totalExpectedRepayment.toString() - binding.tvTotalDue.text = loanWithAssociations.summary.totalOutstanding.toString() - binding.tvTotalPaid.text = loanWithAssociations.summary.totalRepayment.toString() + private fun loadLoanDataTables() { + val action = + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToDataTableFragment( + Constants.DATA_TABLE_NAME_LOANS, + loanAccountNumber + ) + findNavController().navigate(action) } private fun loadDocuments() { val action = LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToDocumentListFragment( - loanAccountNumber, - Constants.ENTITY_TYPE_LOANS + loanAccountNumber, Constants.ENTITY_TYPE_LOANS ) findNavController().navigate(action) } @@ -215,155 +92,53 @@ class LoanAccountSummaryFragment : ProgressableFragment() { private fun loadLoanCharges() { val action = LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanChargeFragment( - loanAccountNumber, - chargesList.toTypedArray() + loanAccountNumber, chargesList.toTypedArray() ) findNavController().navigate(action) } - private fun approveLoan() { - val action = clientLoanWithAssociations?.let { - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountApproval( - loanAccountNumber, + private fun makeRepayment(loan: LoanWithAssociations) { + val action = loan.let { + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentFragment( it ) } - action?.let { findNavController().navigate(it) } + action.let { findNavController().navigate(it) } } - private fun disburseLoan() { - val action = - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountDisbursementFragment( - loanAccountNumber - ) - findNavController().navigate(action) - } - - private fun loadLoanDataTables() { + private fun loadLoanTransactions(loanId: Int) { val action = - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToDataTableFragment( - Constants.DATA_TABLE_NAME_LOANS, - loanAccountNumber + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanTransactionsFragment( + loanId ) findNavController().navigate(action) } - private fun showLoanById(loanWithAssociations: LoanWithAssociations) { - /* Activity is null - Fragment has been detached; no need to do anything. */ - if (activity == null) return - clientLoanWithAssociations = loanWithAssociations - binding.tvClientName.text = loanWithAssociations.clientName - binding.tvLoanProductShortName.text = loanWithAssociations.loanProductName - binding.tvLoanAccountNumber.text = "#" + loanWithAssociations.accountNo - binding.tvLoanOfficer.text = loanWithAssociations.loanOfficerName - //TODO Implement QuickContactBadge - //quickContactBadge.setImageToDefault(); - binding.btProcessLoanTransaction.isEnabled = true - if (loanWithAssociations.status.active == true) { - inflateLoanSummary(loanWithAssociations) - // if Loan is already active - // the Transaction Would be Make Repayment - binding.viewStatusIndicator.setBackgroundColor( - ContextCompat.getColor(requireActivity(), R.color.light_green) - ) - binding.btProcessLoanTransaction.text = "Make Repayment" - processLoanTransactionAction = TRANSACTION_REPAYMENT - } else if (loanWithAssociations.status.pendingApproval == true) { - // if Loan is Pending for Approval - // the Action would be Approve Loan - binding.viewStatusIndicator.setBackgroundColor( - ContextCompat.getColor(requireActivity(), R.color.light_yellow) - ) - binding.btProcessLoanTransaction.text = "Approve Loan" - processLoanTransactionAction = ACTION_APPROVE_LOAN - } else if (loanWithAssociations.status.waitingForDisbursal == true) { - // if Loan is Waiting for Disbursal - // the Action would be Disburse Loan - binding.viewStatusIndicator.setBackgroundColor( - ContextCompat.getColor(requireActivity(), R.color.blue) - ) - binding.btProcessLoanTransaction.text = "Disburse Loan" - processLoanTransactionAction = ACTION_DISBURSE_LOAN - } else if (loanWithAssociations.status.closedObligationsMet == true) { - inflateLoanSummary(loanWithAssociations) - // if Loan is Closed after the obligations are met - // the make payment will be disabled so that no more payment can be collected - binding.viewStatusIndicator.setBackgroundColor( - ContextCompat.getColor(requireActivity(), R.color.black) - ) - binding.btProcessLoanTransaction.isEnabled = false - binding.btProcessLoanTransaction.text = "Make Repayment" - } else { - inflateLoanSummary(loanWithAssociations) - binding.viewStatusIndicator.setBackgroundColor( - ContextCompat.getColor(requireActivity(), R.color.black) - ) - binding.btProcessLoanTransaction.isEnabled = false - binding.btProcessLoanTransaction.text = "Loan Closed" - } - } - - private fun showFetchingError(s: String?) { - Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() - } - - private fun showProgressbar(b: Boolean) { - showProgress(b) - } - - override fun onSaveInstanceState(savedInstanceState: Bundle) { - super.onSaveInstanceState(savedInstanceState) - savedInstanceState.putParcelable("LoanWithAssociation", clientLoanWithAssociations) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - if (savedInstanceState != null) { - // Restore last state for checked position. - clientLoanWithAssociations = savedInstanceState.getParcelable("LoanWithAssociation") - } - } - - private fun makeRepayment(loan: LoanWithAssociations?) { - val action = loan?.let { - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentFragment( + private fun approveLoan(loanWithAssociations: LoanWithAssociations) { + val action = loanWithAssociations.let { + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountApproval( + loanAccountNumber, it ) } - action?.let { findNavController().navigate(it) } + action.let { findNavController().navigate(it) } } - private fun loadRepaymentSchedule(loanId: Int) { + private fun disburseLoan() { val action = - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentScheduleFragment( - loanId + LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountDisbursementFragment( + loanAccountNumber ) findNavController().navigate(action) } - private fun loadLoanTransactions(loanId: Int) { - val action = - LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanTransactionsFragment( - loanId - ) - findNavController().navigate(action) + override fun onResume() { + super.onResume() + toolbar?.visibility = View.GONE } - companion object { - const val MENU_ITEM_DATA_TABLES = 1001 - const val MENU_ITEM_REPAYMENT_SCHEDULE = 1002 - const val MENU_ITEM_LOAN_TRANSACTIONS = 1003 - const val MENU_ITEM_DOCUMENTS = 1004 - const val MENU_ITEM_CHARGES = 1005 - - /* - Set of Actions and Transactions that can be performed depending on the status of the Loan - Actions are performed to change the status of the loan - Transactions are performed to do repayments - */ - private const val ACTION_NOT_SET = -1 - private const val ACTION_APPROVE_LOAN = 0 - private const val ACTION_DISBURSE_LOAN = 1 - private const val TRANSACTION_REPAYMENT = 2 + 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/loanaccountsummary/LoanAccountSummaryFragmentOld.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragmentOld.kt new file mode 100644 index 00000000000..cfb8b0b3d18 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryFragmentOld.kt @@ -0,0 +1,370 @@ +///* +// * 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.loanaccountsummary +// +//import android.os.Bundle +//import android.util.Log +//import android.view.LayoutInflater +//import android.view.Menu +//import android.view.MenuItem +//import android.view.View +//import android.view.ViewGroup +//import android.widget.Toast +//import androidx.core.content.ContextCompat +//import androidx.lifecycle.ViewModelProvider +//import androidx.navigation.fragment.findNavController +//import androidx.navigation.fragment.navArgs +//import com.mifos.core.common.utils.Constants +//import com.mifos.core.objects.accounts.loan.LoanWithAssociations +//import com.mifos.core.objects.client.Charges +//import com.mifos.mifosxdroid.R +//import com.mifos.mifosxdroid.core.ProgressableFragment +//import com.mifos.mifosxdroid.databinding.FragmentLoanAccountSummaryBinding +//import com.mifos.utils.DateHelper +//import dagger.hilt.android.AndroidEntryPoint +// +///** +// * Created by ishankhanna on 09/05/14. +// */ +//@AndroidEntryPoint +//class LoanAccountSummaryFragmentOld : ProgressableFragment() { +// +// private lateinit var binding: FragmentLoanAccountSummaryBinding +// private val arg: LoanAccountSummaryFragmentArgs by navArgs() +// +// var loanAccountNumber = 0 +// +// private lateinit var viewModel: LoanAccountSummaryViewModel +// +// var chargesList: MutableList = ArrayList() +// +// // Action Identifier in the onProcessTransactionClicked Method +// private var processLoanTransactionAction = -1 +// private var parentFragment = true +// private var clientLoanWithAssociations: LoanWithAssociations? = null +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// loanAccountNumber = arg.loanAccountNumber +// parentFragment = arg.parentFragment +// //Necessary Call to add and update the Menu in a Fragment +// setHasOptionsMenu(true) +// } +// +// override fun onCreateView( +// inflater: LayoutInflater, +// container: ViewGroup?, +// savedInstanceState: Bundle? +// ): View { +// binding = FragmentLoanAccountSummaryBinding.inflate(inflater, container, false) +// viewModel = ViewModelProvider(this)[LoanAccountSummaryViewModel::class.java] +// inflateLoanAccountSummary() +// +// viewModel.loanAccountSummaryUiState.observe(viewLifecycleOwner) { +// when (it) { +// is LoanAccountSummaryUiState.ShowFetchingError -> { +// showProgressbar(false) +// showFetchingError(it.message) +// } +// +// is LoanAccountSummaryUiState.ShowLoanById -> { +// showProgressbar(false) +// showLoanById(it.loanWithAssociations) +// } +// +// is LoanAccountSummaryUiState.ShowProgressbar -> showProgressbar(true) +// } +// } +// +// return binding.root +// } +// +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// super.onViewCreated(view, savedInstanceState) +// +// binding.btProcessLoanTransaction.setOnClickListener { +// onProcessTransactionClicked() +// } +// } +// +// private fun inflateLoanAccountSummary() { +// showProgress(true) +// setToolbarTitle(resources.getString(R.string.loanAccountSummary)) +// //TODO Implement cases to enable/disable repayment button +// binding.btProcessLoanTransaction.isEnabled = false +// viewModel.loadLoanById(loanAccountNumber) +// } +// +// private fun onProcessTransactionClicked() { +// when (processLoanTransactionAction) { +// TRANSACTION_REPAYMENT -> { +// makeRepayment(clientLoanWithAssociations) +// } +// +// ACTION_APPROVE_LOAN -> { +// approveLoan() +// } +// +// ACTION_DISBURSE_LOAN -> { +// disburseLoan() +// } +// +// else -> { +// Log.i(requireActivity().localClassName, "TRANSACTION ACTION NOT SET") +// } +// } +// } +// +// override fun onDetach() { +// super.onDetach() +// if (!parentFragment) { +// requireActivity().finish() +// } +// } +// +// override fun onPrepareOptionsMenu(menu: Menu) { +// menu.clear() +// menu.add(Menu.NONE, MENU_ITEM_DATA_TABLES, Menu.NONE, Constants.DATA_TABLE_LOAN_NAME) +// menu.add( +// Menu.NONE, +// MENU_ITEM_LOAN_TRANSACTIONS, +// Menu.NONE, +// resources.getString(R.string.transactions) +// ) +// menu.add( +// Menu.NONE, +// MENU_ITEM_REPAYMENT_SCHEDULE, +// Menu.NONE, +// resources.getString(R.string.loan_repayment_schedule) +// ) +// menu.add(Menu.NONE, MENU_ITEM_DOCUMENTS, Menu.NONE, resources.getString(R.string.documents)) +// menu.add(Menu.NONE, MENU_ITEM_CHARGES, Menu.NONE, resources.getString(R.string.charges)) +// super.onPrepareOptionsMenu(menu) +// } +// +// override fun onOptionsItemSelected(item: MenuItem): Boolean { +// val id = item.itemId +// when (id) { +// MENU_ITEM_REPAYMENT_SCHEDULE -> loadRepaymentSchedule(loanAccountNumber) +// MENU_ITEM_LOAN_TRANSACTIONS -> loadLoanTransactions(loanAccountNumber) +// MENU_ITEM_DOCUMENTS -> loadDocuments() +// MENU_ITEM_CHARGES -> loadLoanCharges() +// MENU_ITEM_DATA_TABLES -> loadLoanDataTables() +// else -> { +// } +// } +// return super.onOptionsItemSelected(item) +// } +// +// private fun inflateLoanSummary(loanWithAssociations: LoanWithAssociations) { +// binding.tvAmountDisbursed.text = loanWithAssociations.summary +// .principalDisbursed.toString() +// try { +// binding.tvDisbursementDate.text = loanWithAssociations +// .timeline.actualDisbursementDate?.let { +// DateHelper.getDateAsString( +// it as List +// ) +// } +// } catch (exception: IndexOutOfBoundsException) { +// Toast.makeText( +// activity, +// resources.getString(R.string.loan_rejected_message), +// Toast.LENGTH_SHORT +// ).show() +// } +// binding.tvInArrears.text = loanWithAssociations.summary.totalOverdue.toString() +// binding.tvPrincipal.text = loanWithAssociations.summary +// .principalDisbursed.toString() +// binding.tvLoanPrincipalDue.text = loanWithAssociations.summary +// .principalOutstanding.toString() +// binding.tvLoanPrincipalPaid.text = loanWithAssociations.summary +// .principalPaid.toString() +// binding.tvInterest.text = loanWithAssociations.summary.interestCharged.toString() +// binding.tvLoanInterestDue.text = loanWithAssociations.summary +// .interestOutstanding.toString() +// binding.tvLoanInterestPaid.text = loanWithAssociations.summary +// .interestPaid.toString() +// binding.tvFees.text = loanWithAssociations.summary.feeChargesCharged.toString() +// binding.tvLoanFeesDue.text = loanWithAssociations.summary +// .feeChargesOutstanding.toString() +// binding.tvLoanFeesPaid.text = loanWithAssociations.summary +// .feeChargesPaid.toString() +// binding.tvPenalty.text = loanWithAssociations.summary +// .penaltyChargesCharged.toString() +// binding.tvLoanPenaltyDue.text = loanWithAssociations.summary +// .penaltyChargesOutstanding.toString() +// binding.tvLoanPenaltyPaid.text = loanWithAssociations.summary +// .penaltyChargesPaid.toString() +// binding.tvTotal.text = loanWithAssociations.summary +// .totalExpectedRepayment.toString() +// binding.tvTotalDue.text = loanWithAssociations.summary.totalOutstanding.toString() +// binding.tvTotalPaid.text = loanWithAssociations.summary.totalRepayment.toString() +// } +// +// private fun loadDocuments() { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToDocumentListFragment( +// loanAccountNumber, +// Constants.ENTITY_TYPE_LOANS +// ) +// findNavController().navigate(action) +// } +// +// private fun loadLoanCharges() { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanChargeFragment( +// loanAccountNumber, +// chargesList.toTypedArray() +// ) +// findNavController().navigate(action) +// } +// +// private fun approveLoan() { +// val action = clientLoanWithAssociations?.let { +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountApproval( +// loanAccountNumber, +// it +// ) +// } +// action?.let { findNavController().navigate(it) } +// } +// +// private fun disburseLoan() { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanAccountDisbursementFragment( +// loanAccountNumber +// ) +// findNavController().navigate(action) +// } +// +// private fun loadLoanDataTables() { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToDataTableFragment( +// Constants.DATA_TABLE_NAME_LOANS, +// loanAccountNumber +// ) +// findNavController().navigate(action) +// } +// +// private fun showLoanById(loanWithAssociations: LoanWithAssociations) { +// /* Activity is null - Fragment has been detached; no need to do anything. */ +// if (activity == null) return +// clientLoanWithAssociations = loanWithAssociations +// binding.tvClientName.text = loanWithAssociations.clientName +// binding.tvLoanProductShortName.text = loanWithAssociations.loanProductName +// binding.tvLoanAccountNumber.text = "#" + loanWithAssociations.accountNo +// binding.tvLoanOfficer.text = loanWithAssociations.loanOfficerName +// //TODO Implement QuickContactBadge +// //quickContactBadge.setImageToDefault(); +// binding.btProcessLoanTransaction.isEnabled = true +// if (loanWithAssociations.status.active == true) { +// inflateLoanSummary(loanWithAssociations) +// // if Loan is already active +// // the Transaction Would be Make Repayment +// binding.viewStatusIndicator.setBackgroundColor( +// ContextCompat.getColor(requireActivity(), R.color.light_green) +// ) +// binding.btProcessLoanTransaction.text = "Make Repayment" +// processLoanTransactionAction = TRANSACTION_REPAYMENT +// } else if (loanWithAssociations.status.pendingApproval == true) { +// // if Loan is Pending for Approval +// // the Action would be Approve Loan +// binding.viewStatusIndicator.setBackgroundColor( +// ContextCompat.getColor(requireActivity(), R.color.light_yellow) +// ) +// binding.btProcessLoanTransaction.text = "Approve Loan" +// processLoanTransactionAction = ACTION_APPROVE_LOAN +// } else if (loanWithAssociations.status.waitingForDisbursal == true) { +// // if Loan is Waiting for Disbursal +// // the Action would be Disburse Loan +// binding.viewStatusIndicator.setBackgroundColor( +// ContextCompat.getColor(requireActivity(), R.color.blue) +// ) +// binding.btProcessLoanTransaction.text = "Disburse Loan" +// processLoanTransactionAction = ACTION_DISBURSE_LOAN +// } else if (loanWithAssociations.status.closedObligationsMet == true) { +// inflateLoanSummary(loanWithAssociations) +// // if Loan is Closed after the obligations are met +// // the make payment will be disabled so that no more payment can be collected +// binding.viewStatusIndicator.setBackgroundColor( +// ContextCompat.getColor(requireActivity(), R.color.black) +// ) +// binding.btProcessLoanTransaction.isEnabled = false +// binding.btProcessLoanTransaction.text = "Make Repayment" +// } else { +// inflateLoanSummary(loanWithAssociations) +// binding.viewStatusIndicator.setBackgroundColor( +// ContextCompat.getColor(requireActivity(), R.color.black) +// ) +// binding.btProcessLoanTransaction.isEnabled = false +// binding.btProcessLoanTransaction.text = "Loan Closed" +// } +// } +// +// private fun showFetchingError(s: String?) { +// Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() +// } +// +// private fun showProgressbar(b: Boolean) { +// showProgress(b) +// } +// +// override fun onSaveInstanceState(savedInstanceState: Bundle) { +// super.onSaveInstanceState(savedInstanceState) +// savedInstanceState.putParcelable("LoanWithAssociation", clientLoanWithAssociations) +// } +// +// override fun onActivityCreated(savedInstanceState: Bundle?) { +// super.onActivityCreated(savedInstanceState) +// if (savedInstanceState != null) { +// // Restore last state for checked position. +// clientLoanWithAssociations = savedInstanceState.getParcelable("LoanWithAssociation") +// } +// } +// +// private fun makeRepayment(loan: LoanWithAssociations?) { +// val action = loan?.let { +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentFragment( +// it +// ) +// } +// action?.let { findNavController().navigate(it) } +// } +// +// private fun loadRepaymentSchedule(loanId: Int) { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanRepaymentScheduleFragment( +// loanId +// ) +// findNavController().navigate(action) +// } +// +// private fun loadLoanTransactions(loanId: Int) { +// val action = +// LoanAccountSummaryFragmentDirections.actionLoanAccountSummaryFragmentToLoanTransactionsFragment( +// loanId +// ) +// findNavController().navigate(action) +// } +// +// companion object { +// const val MENU_ITEM_DATA_TABLES = 1001 +// const val MENU_ITEM_REPAYMENT_SCHEDULE = 1002 +// const val MENU_ITEM_LOAN_TRANSACTIONS = 1003 +// const val MENU_ITEM_DOCUMENTS = 1004 +// const val MENU_ITEM_CHARGES = 1005 +// +// /* +// Set of Actions and Transactions that can be performed depending on the status of the Loan +// Actions are performed to change the status of the loan +// Transactions are performed to do repayments +// */ +// private const val ACTION_NOT_SET = -1 +// private const val ACTION_APPROVE_LOAN = 0 +// private const val ACTION_DISBURSE_LOAN = 1 +// private const val TRANSACTION_REPAYMENT = 2 +// } +//} +// diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryScreen.kt new file mode 100644 index 00000000000..b28caf3b5e8 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryScreen.kt @@ -0,0 +1,616 @@ +package com.mifos.mifosxdroid.online.loanaccountsummary + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +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.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.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.LocalContext +import androidx.compose.ui.res.stringResource +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.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.common.utils.Constants +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosMenuDropDownItem +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.BluePrimary +import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.designsystem.theme.DarkGray +import com.mifos.core.designsystem.theme.White +import com.mifos.core.objects.accounts.loan.LoanWithAssociations +import com.mifos.core.objects.accounts.loan.Status +import com.mifos.core.objects.accounts.loan.Summary +import com.mifos.mifosxdroid.R +import com.mifos.utils.DateHelper + +/** + * Created by Pronay Sarker on 01/07/2024 (5:50 AM) + */ + + +@Composable +fun LoanAccountSummaryScreen( + viewModel: LoanAccountSummaryViewModel = hiltViewModel(), + loanAccountNumber: Int, + navigateBack: () -> Unit, + onMoreInfoClicked: () -> Unit, + onTransactionsClicked: (loadId: Int) -> Unit, + onRepaymentScheduleClicked: (loanId: Int) -> Unit, + onDocumentsClicked: () -> Unit, + onChargesClicked: () -> Unit, + approveLoan: (loanWithAssociations: LoanWithAssociations) -> Unit, + disburseLoan: () -> Unit, + onRepaymentClick: (loanWithAssociations: LoanWithAssociations) -> Unit +) { + val uiState by viewModel.loanAccountSummaryUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + viewModel.loadLoanById(loanAccountNumber) + } + + LoanAccountSummaryScreen( + uiState = uiState, + navigateBack = navigateBack, + onRetry = { viewModel.loadLoanById(loanAccountNumber) }, + onMoreInfoClicked = onMoreInfoClicked, + onTransactionsClicked = { onTransactionsClicked.invoke(loanAccountNumber) }, + onRepaymentScheduleClicked = { onRepaymentScheduleClicked.invoke(loanAccountNumber) }, + onDocumentsClicked = onDocumentsClicked, + onChargesClicked = onChargesClicked, + approveLoan = approveLoan, + disburseLoan = disburseLoan, + makeRepayment = onRepaymentClick + ) +} + +@Composable +fun LoanAccountSummaryScreen( + uiState: LoanAccountSummaryUiState, + navigateBack: () -> Unit, + onRetry: () -> Unit, + onMoreInfoClicked: () -> Unit, + onTransactionsClicked: () -> Unit, + onRepaymentScheduleClicked: () -> Unit, + onDocumentsClicked: () -> Unit, + onChargesClicked: () -> Unit, + approveLoan: (loanWithAssociations: LoanWithAssociations) -> Unit, + disburseLoan: () -> Unit, + makeRepayment: (loanWithAssociations: LoanWithAssociations) -> Unit +) { + val snackbarHostState = remember { + androidx.compose.material3.SnackbarHostState() + } + var openDropdown by rememberSaveable { + mutableStateOf(false) + } + + MifosScaffold(icon = MifosIcons.arrowBack, + title = stringResource(id = R.string.loanAccountSummary), + onBackPressed = navigateBack, + snackbarHostState = snackbarHostState, + actions = { + IconButton(onClick = { openDropdown = !openDropdown }) { + Icon( + imageVector = MifosIcons.moreVert, contentDescription = null + ) + } + if (openDropdown) { + DropdownMenu( + expanded = openDropdown, + onDismissRequest = { openDropdown = false } + ) { + MifosMenuDropDownItem( + option = Constants.DATA_TABLE_LOAN_NAME, + onClick = { + openDropdown = false + onMoreInfoClicked.invoke() + } + ) + MifosMenuDropDownItem( + option = stringResource(id = R.string.transactions), + onClick = { + openDropdown = false + onTransactionsClicked.invoke() + } + ) + MifosMenuDropDownItem( + option = stringResource(id = R.string.loan_repayment_schedule), + onClick = { + openDropdown = false + onRepaymentScheduleClicked.invoke() + } + ) + MifosMenuDropDownItem( + option = stringResource(id = R.string.documents), + onClick = { + openDropdown = false + onDocumentsClicked.invoke() + } + ) + MifosMenuDropDownItem( + option = stringResource(id = R.string.charges), + onClick = { + openDropdown = false + onChargesClicked.invoke() + } + ) + } + } + }) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + when (uiState) { + is LoanAccountSummaryUiState.ShowFetchingError -> { + MifosSweetError( + message = uiState.message, onclick = onRetry + ) + } + + is LoanAccountSummaryUiState.ShowLoanById -> { + val loanWithAssociations = uiState.loanWithAssociations + LoanAccountSummaryContent( + loanWithAssociations = loanWithAssociations, + makeRepayment = { makeRepayment.invoke(loanWithAssociations) }, + approveLoan = { approveLoan.invoke(loanWithAssociations) }, + disburseLoan = disburseLoan + ) + } + + LoanAccountSummaryUiState.ShowProgressbar -> { + MifosCircularProgress() + } + } + } + } +} + +@Composable +fun LoanAccountSummaryContent( + loanWithAssociations: LoanWithAssociations, + makeRepayment: () -> Unit, + approveLoan: () -> Unit, + disburseLoan: () -> Unit, +) { + val context = LocalContext.current + val inflateLoanSummary = getInflateLoanSummaryValue(status = loanWithAssociations.status) + val scrollState = rememberScrollState() + + fun getActualDisbursementDateInStringFormat(): String { + try { + return loanWithAssociations.timeline.actualDisbursementDate?.let { + DateHelper.getDateAsString(it as List) + } ?: "" + } catch (exception: IndexOutOfBoundsException) { + Toast.makeText( + context, + context.resources.getString(R.string.loan_rejected_message), + Toast.LENGTH_SHORT + ).show() + return "" + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .verticalScroll(scrollState) + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp), + text = loanWithAssociations.clientName, + style = MaterialTheme.typography.bodyLarge, + ) + + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Canvas(modifier = Modifier + .size(22.dp) + .padding(top = 4.dp, end = 4.dp), + contentDescription = "", + onDraw = { + drawRect( + color = when { + loanWithAssociations.status.active == true -> { + Color.Green + } + + loanWithAssociations.status.pendingApproval == true -> { + Color.Yellow + } + + loanWithAssociations.status.waitingForDisbursal == true -> { + Color.Blue + } + + else -> { + Color.Black + } + } + ) + }) + + LoanSummaryFarApartTextItem( + title = loanWithAssociations.loanProductName, + value = "#" + loanWithAssociations.accountNo + ) + } + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp) + ) + + LoanSummaryFarApartTextItem( + title = stringResource(id = R.string.loan_amount_disbursed), + value = if (inflateLoanSummary) loanWithAssociations.summary.principalDisbursed?.toString() + ?: "" else "" + ) + + LoanSummaryFarApartTextItem( + title = stringResource(id = R.string.loan_disbursement_date), + value = if (inflateLoanSummary) getActualDisbursementDateInStringFormat() else "" + ) + + LoanSummaryFarApartTextItem( + title = stringResource(id = R.string.loan_in_arrears), + value = if (inflateLoanSummary) loanWithAssociations.summary.totalOverdue?.toString() + ?: "" else "" + ) + + LoanSummaryFarApartTextItem( + title = stringResource(id = R.string.staff), + value = loanWithAssociations.loanOfficerName + ) + + Spacer(modifier = Modifier.height(8.dp)) + + LoanSummaryDataTable( + loanSummary = loanWithAssociations.summary, + inflateLoanSummary = inflateLoanSummary + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Button( + enabled = getButtonActiveStatus(loanWithAssociations.status), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .height(45.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, + ), + onClick = when { + loanWithAssociations.status.active == true -> { + { makeRepayment.invoke() } + } + + loanWithAssociations.status.pendingApproval == true -> { + { approveLoan.invoke() } + } + + loanWithAssociations.status.waitingForDisbursal == true -> { + { disburseLoan.invoke() } + } + + loanWithAssociations.status.closedObligationsMet == true -> { + { Log.d("LoanAccountSummary", "TRANSACTION ACTION NOT SET") } + } + + else -> { + { Log.d("LoanAccountSummary", "TRANSACTION ACTION NOT SET") } + } + } + ) { + Text( + color = MaterialTheme.colorScheme.background, + text = getButtonText(context, loanWithAssociations.status) + ) + } + } +} + +@Composable +fun LoanSummaryDataTable(loanSummary: Summary, inflateLoanSummary: Boolean) { + // dataTable should be empty if [inflateLoanSummary] is false + val summary = if (inflateLoanSummary) loanSummary else null + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.summary), + loanColumnValue = stringResource(id = R.string.loan), + amountColumnValue = stringResource(id = R.string.amount_paid), + balanceColumnValue = stringResource(id = R.string.balance), + isHeader = true, + color = BluePrimary.copy(alpha = .3f) + ) + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.loan_principal), + loanColumnValue = summary?.principalDisbursed?.toString() ?: "", + amountColumnValue = summary?.principalPaid?.toString() ?: "", + balanceColumnValue = summary?.principalOutstanding?.toString() ?: "" + ) + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.loan_interest), + loanColumnValue = summary?.interestCharged?.toString() ?: "", + amountColumnValue = summary?.interestPaid?.toString() ?: "", + balanceColumnValue = summary?.interestOutstanding?.toString() ?: "", + color = BluePrimary.copy(alpha = .1f) + ) + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.loan_fees), + loanColumnValue = summary?.feeChargesCharged?.toString() ?: "", + amountColumnValue = summary?.feeChargesPaid?.toString() ?: "", + balanceColumnValue = summary?.feeChargesOutstanding?.toString() ?: "" + ) + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.loan_penalty), + loanColumnValue = summary?.penaltyChargesCharged?.toString() ?: "", + amountColumnValue = summary?.penaltyChargesPaid?.toString() ?: "", + balanceColumnValue = summary?.penaltyChargesOutstanding?.toString() ?: "", + color = BluePrimary.copy(alpha = .1f) + ) + + DataTableRow( + summaryColumnTitle = stringResource(id = R.string.total), + loanColumnValue = summary?.totalExpectedRepayment?.toString() ?: "", + amountColumnValue = summary?.totalRepayment?.toString() ?: "", + balanceColumnValue = summary?.totalOutstanding?.toString() ?: "" + ) +} + +@Composable +fun LoanSummaryFarApartTextItem(title: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = title, + color = Black, + ) + + Text( + style = MaterialTheme.typography.bodyLarge, + text = value, + color = DarkGray, + ) + } +} + +@Composable +private fun DataTableRow( + summaryColumnTitle: String, + loanColumnValue: String, + amountColumnValue: String, + balanceColumnValue: String, + isHeader: Boolean = false, + color: Color = White +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(color), + ) { + Text( + text = summaryColumnTitle, + modifier = Modifier + .weight(2.5f) + .padding(vertical = 6.dp) + .padding(start = 2.dp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = if (isHeader) FontWeight.Bold else FontWeight.Normal, + ) + + Text( + text = loanColumnValue, + modifier = Modifier + .weight(2.8f) + .padding(horizontal = 6.dp, vertical = 6.dp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = if (isHeader) FontWeight.Bold else FontWeight.Normal, + textAlign = TextAlign.End, + ) + + Text( + text = amountColumnValue, + modifier = Modifier + .weight(2.7f) + .padding(end = 6.dp, top = 6.dp, bottom = 6.dp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = if (isHeader) FontWeight.Bold else FontWeight.Normal, + textAlign = TextAlign.End, + ) + + Text( + text = balanceColumnValue, + modifier = Modifier + .weight(2f) + .padding(vertical = 6.dp) + .padding(end = 2.dp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = if (isHeader) FontWeight.Bold else FontWeight.Normal, + textAlign = TextAlign.End + ) + } +} + +private fun getButtonText(context: Context, status: Status): String { + return when { + status.active == true || status.closedObligationsMet == true -> { + context.resources.getString(R.string.make_Repayment) + } + + status.pendingApproval == true -> { + context.resources.getString(R.string.approve_loan) + } + + status.waitingForDisbursal == true -> { + context.resources.getString(R.string.disburse_loan) + } + + else -> { + context.resources.getString(R.string.loan_closed) + } + } +} + +private fun getButtonActiveStatus(status: Status): Boolean { + return when { + status.active == true || status.pendingApproval == true || status.waitingForDisbursal == true -> { + true + } + + else -> { + false + } + } +} + +@Composable +fun getInflateLoanSummaryValue(status: Status): Boolean { + return when { + status.active == true || status.closedObligationsMet == true -> { + true + } + + status.pendingApproval == true || status.waitingForDisbursal == true -> { + false + } + + else -> { + true + } + } +} + +class LoanAccountSummaryPreviewProvider : PreviewParameterProvider { + val demoSummary = Summary( + loanId = 12345, + principalDisbursed = 10000.0, + principalOutstanding = 6000.0, + principalOverdue = 500.0, + interestCharged = 500.0, + interestPaid = 300.0, + interestWaived = 0.0, + interestWrittenOff = 0.0, + interestOutstanding = 200.0, + interestOverdue = 50.0, + feeChargesCharged = 200.0, + feeChargesDueAtDisbursementCharged = 50.0, + feeChargesPaid = 150.0, + feeChargesWaived = 0.0, + feeChargesWrittenOff = 0.0, + feeChargesOutstanding = 50.0, + feeChargesOverdue = 20.0, + penaltyChargesCharged = 100.0, + penaltyChargesPaid = 50.0, + penaltyChargesWaived = 0.0, + penaltyChargesWrittenOff = 0.0, + penaltyChargesOutstanding = 50.0, + penaltyChargesOverdue = 10.0, + totalExpectedRepayment = 10700.0, + totalRepayment = 4450.0, + totalExpectedCostOfLoan = 750.0, + totalCostOfLoan = 300.0, + totalOutstanding = 6250.0, + totalOverdue = 580.0, + overdueSinceDate = listOf(2024, 6, 1) + ) + + override val values: Sequence + get() = sequenceOf( + LoanAccountSummaryUiState.ShowProgressbar, + LoanAccountSummaryUiState.ShowFetchingError("Could not fetch summary"), + LoanAccountSummaryUiState.ShowLoanById( + LoanWithAssociations( + accountNo = "90927493938", + status = Status( + closedObligationsMet = true + ), + clientName = "Pronay sarker", + loanOfficerName = "MR. Ching chong", + loanProductName = "Group Loan", + summary = demoSummary + ) + ), + ) +} + +@Composable +@Preview(showSystemUi = true) +fun PreviewLoanAccountSummary( + @PreviewParameter(LoanAccountSummaryPreviewProvider::class) loanAccountSummaryUiState: LoanAccountSummaryUiState +) { + LoanAccountSummaryScreen( + uiState = loanAccountSummaryUiState, + navigateBack = { }, + onRetry = { }, + onMoreInfoClicked = { }, + onTransactionsClicked = { }, + onRepaymentScheduleClicked = { }, + onDocumentsClicked = { }, + onChargesClicked = { }, + approveLoan = { }, + disburseLoan = { }, + makeRepayment = { } + ) +} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryUiState.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryUiState.kt index 4f9004197b4..2bf019a3c8c 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryUiState.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryUiState.kt @@ -11,6 +11,5 @@ sealed class LoanAccountSummaryUiState { data class ShowFetchingError(val message: String) : LoanAccountSummaryUiState() - data class ShowLoanById(val loanWithAssociations: LoanWithAssociations) : - LoanAccountSummaryUiState() + data class ShowLoanById(val loanWithAssociations: LoanWithAssociations) : LoanAccountSummaryUiState() } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryViewModel.kt index 1c73807a7c2..21a78a86687 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/loanaccountsummary/LoanAccountSummaryViewModel.kt @@ -4,7 +4,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.mifos.core.objects.accounts.loan.LoanWithAssociations +import com.mifos.feature.note.NoteUiState 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,9 +20,9 @@ import javax.inject.Inject class LoanAccountSummaryViewModel @Inject constructor(private val repository: LoanAccountSummaryRepository) : ViewModel() { - private val _loanAccountSummaryUiState = MutableLiveData() + private val _loanAccountSummaryUiState = MutableStateFlow(LoanAccountSummaryUiState.ShowProgressbar) - val loanAccountSummaryUiState: LiveData + val loanAccountSummaryUiState: StateFlow get() = _loanAccountSummaryUiState @@ -40,7 +43,7 @@ class LoanAccountSummaryViewModel @Inject constructor(private val repository: Lo LoanAccountSummaryUiState.ShowLoanById( it ) - } + }!! } }) } diff --git a/mifosng-android/src/main/res/layout/fragment_loan_account_summary.xml b/mifosng-android/src/main/res/layout/fragment_loan_account_summary.xml index 9a687f7d4ec..6806d743f66 100755 --- a/mifosng-android/src/main/res/layout/fragment_loan_account_summary.xml +++ b/mifosng-android/src/main/res/layout/fragment_loan_account_summary.xml @@ -10,11 +10,11 @@ android:outAnimation="@android:anim/fade_out"> - + + + + + Break Down Loan Accounts Loan Account Summary + Make Repayment + Loan closed + Approve Loan Loan Account Summary Loan Repayment Schedule Loan was Rejected