Skip to content

Conversation

ankitkumarrain
Copy link

@ankitkumarrain ankitkumarrain commented Sep 2, 2025

Fixes - Jira-#575

This is my first pr.
Main Goal : It was to implement collateral data modules in the client profile .

I have created a new directory in which i have CollateralUistate , CollateralViewModel, CollateralScreen under under the feature >loan module .

Comment on lines 42 to 53
Scaffold(
topBar = {
TopAppBar(title = {
val titleText = when (val state = uiState) {
is ClientCollateralUiState.Success -> "Collateral Data (${state.totalItems} ${if (state.totalItems == 1) "Item" else "Items"})"
is ClientCollateralUiState.Empty -> "Collateral Data (0 Items)"
else -> "Collateral Data"
}
Text(text = titleText)
})
}
) { paddingValues ->
Copy link
Contributor

@TheKalpeshPawar TheKalpeshPawar Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read my last three comments first. Then read this one and rest.


You don't have to create your own scaffold, use MIfosScaffold.
Check the code of siblings screens and similar screens to see how we are using it.
If you want to use a component, first check if there is a Mifos component already created for it.
You will find all the created components in the ui module in core module.

You can check the sibling feature screens to see what component they are using. If you don't find one only then create a new component in the current screen's package.

Comment on lines 101 to 121
fun CollateralListItem(item: CollateralDisplayItem, onActionClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Type/Name: ${item.typeName}", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text("Quantity: ${item.quantity}", style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(4.dp))
Text("Unit Value: ${item.unitValue}", style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(4.dp))
Text("Total Collateral Value: ${item.totalCollateralValue}", style = MaterialTheme.typography.bodyMedium)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
IconButton(onClick = onActionClick) {

}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use MIfosActiosCollateralDataListingComponent composable here.
It's an already created composable in inside the package com.mifos.core.ui.components.

Comment on lines 137 to 152

@Composable
fun ErrorState(message: String, onRetry: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(16.dp)
) {
Text("Error: $message", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onRetry) {
Text("Retry")
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the MifosErrorComponent here like this

@Composable
private fun ClientProfileDialogs(
    state: ClientProfileState,
    onRetry: () -> Unit,
) {
    when (state.dialogState) {
        is ClientProfileState.DialogState.Loading -> MifosProgressIndicator()

        is ClientProfileState.DialogState.Error -> {
            MifosErrorComponent(
                isNetworkConnected = state.networkConnection,
                message = state.dialogState.message,
                isRetryEnabled = true,
                onRetry = {
                    onRetry()
                },
            )
        }

        null -> Unit
    }
}

Comment on lines 123 to 136
@Composable
fun EmptyCollateralState() {
Card(modifier = Modifier.padding(16.dp)) {
Column(
modifier = Modifier
.padding(32.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("No Item Found", style = MaterialTheme.typography.headlineSmall)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use MifosEmptyCard instead.

like this

if (state.recurringDepositAccounts.isEmpty()) {
     MifosEmptyCard(msg = stringResource(Res.string.client_empty_card_message))
} else {
     LazyColumn {

Comment on lines 1 to 78
package com.mifos.feature.loan.ClientCollateral

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.mifos.core.common.utils.DataState
import com.mifos.core.data.repository.ClientDetailsRepository

import com.mifos.core.network.model.CollateralItem
import com.mifos.feature.loan.ClientCollateral.ClientCollateralUiState.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class ClientCollateralViewModel(
private val savedStateHandle: SavedStateHandle, // Assuming we might need clientId/groupId from nav args
private val clientDetailsRepository: ClientDetailsRepository
) : ViewModel() {

private val _uiState = MutableStateFlow<ClientCollateralUiState>(ClientCollateralUiState.Loading)
val uiState: StateFlow<ClientCollateralUiState> = _uiState.asStateFlow()


private val clientId: Int? = savedStateHandle.get<Int>("clientIdKey")

init {
loadCollateralItems()
}

fun loadCollateralItems() {
if (clientId == null) {
_uiState.value = ClientCollateralUiState.Error("Client ID not found")
return
}

viewModelScope.launch {
_uiState.value = ClientCollateralUiState.Loading
when (val result = clientDetailsRepository.getCollateralItems()) {
is DataState.Success -> {
val networkItems = result.data
if (networkItems.isEmpty()) {
_uiState.value = ClientCollateralUiState.Empty
} else {
val displayItems = networkItems.mapNotNull { transformToDisplayItem(it) }
if (displayItems.isEmpty() && networkItems.isNotEmpty()) {
// This case means all items failed to parse quantity, which is an error
_uiState.value = Error("Error parsing collateral data")
} else {
_uiState.value = Success(displayItems, displayItems.size)
}
}
}
is DataState.Error -> {
_uiState.value = Error(result.exception.message ?: "Unknown error")
}

DataState.Loading -> TODO()
}
}
}

private fun transformToDisplayItem(networkItem: CollateralItem): CollateralDisplayItem? {
val quantity = networkItem.quality.toIntOrNull()
return if (quantity != null) {
CollateralDisplayItem(
id = networkItem.id,
typeName = networkItem.name,
quantity = quantity,
unitValue = networkItem.basePrice,
totalCollateralValue = quantity * networkItem.basePrice
)
} else {

null
}
}
}
Copy link
Contributor

@TheKalpeshPawar TheKalpeshPawar Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ankitkumarrain
search on internet about MVI architecture and learn about it.
See how and why we separate ui actions, events and ui state.
Then see inside the viewmodel of other screens, how we are doing it.

If you need help then just ask.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have to implement mvvm pattern

Comment on lines 16 to 20
class ClientCollateralViewModel(
private val savedStateHandle: SavedStateHandle, // Assuming we might need clientId/groupId from nav args
private val clientDetailsRepository: ClientDetailsRepository
) : ViewModel() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use BaseViewModel class. It is present inside the com.mifos.core.ui.util package.

import androidx.lifecycle.compose.collectAsStateWithLifecycle
// Koin ViewModel import
import org.koin.compose.viewmodel.koinViewModel

Copy link
Contributor

@TheKalpeshPawar TheKalpeshPawar Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You needed help with navigation right.

So, in this project we are using type safe navigation and also nested nav graphs.

I will give you a simple analogy.
Consider a Navgraph like your house (collection of multiple rooms). All houses have a door to enter inside, (now don't think what about windows or balcony), and it always opens inside one room always. Just like that a navgraph is a collection of multiple navigation destinations (screen). When you navigate to a navgraph there is always a screen set a startdestination that opens first..
And just like from inside of your room you can go inside multiple other room, so, just like that you can go to multiple other screens from that screen.

You won't create a navgraph here.

Learn about typesafe navigation and then see how we are using it.
In typesafe navigation you use serialized data classes instead of string route like in web or in normal string based navigation.
Here is an example:

@Serializable
data class ClientProfileRoute(
    val id: Int = -1
)

Instead of using a string you will use such data classes. The arguments you pass along with string routs are instead passed a parameters to the data class.

Also learn about extension functions.

You will create a navigation destination(route) by creating a extension function on the NavGraphBuilder, something like this
NavGraphBuilder.navigateToCleintCollateralRoute

Just look into the client profile screen and see how navigation is done there.

If you need help ask.

@therajanmaurya therajanmaurya changed the title Ticket 575 on android client Client Collateral Datas Sep 3, 2025
Comment on lines 44 to 56
}
try{
val result = repository.getCollateralItems(route.clientId)
mutableStateFlow.update {
it.copy(
isLoading = false,
accounts = result as List<CollateralItem>,
dialogState = null


)
}
}catch(e : Exception){
Copy link
Contributor

@TheKalpeshPawar TheKalpeshPawar Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ankit repository.getCollateralItems(route.clientId) returns DataState<List<CollateralItem>>, and you are casting it as List<CollateriaItem>, that is not a valid operation.

See in other viewmodels how we are handling DataState and how to retrieve data from it.

This is the reason you are not getting any data.

Comment on lines 15 to 33

class ClientCollateralViewmodel (
savedStateHandle: SavedStateHandle,
private val repository: ClientDetailsRepository
): BaseViewModel<collateralUiState,collateralEvent,collateralAction>(
initialState = collateralUiState()
){
private val route = savedStateHandle.toRoute<clientCollateralRoute>()
override fun handleAction(action: collateralAction) {
when (action) {
is collateralAction.cardClicked -> handleCardClicked(action.activeIndex)
collateralAction.toggleFiler -> toggleFiler()
collateralAction.toggleSearchBar -> toggleSearchBar()
is collateralAction.viewAccount -> sendEvent(collateralEvent.viewAccount(action.accountId))
collateralAction.refresh -> fetchAllCollateralAccount()

}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always write class, interfaces , composable function and objects name in Pascal Case Eg: CollateralAction.ToggleFilter

Comment on lines 36 to 44
}
private fun fetchAllCollateralAccount(){
viewModelScope.launch {
mutableStateFlow.update{
it.copy(
isLoading = true,
)

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use a separate state parameter for handling loading. See how we are using DialogState for handling Error and Loading on UI.
You will find something like this in other UiState classes.

data class SomeUiState(
    val dialogState: DialogState? = null
 ){
     sealed interface DialogState {
         object Loading: DialogState
         data class Error(val message: String): DialogState
    }
}

Comment on lines 67 to 83
private fun toggleFiler() {
mutableStateFlow.update {
it.copy(
isFilterActive = !state.isFilterActive,
)
}


}
private fun toggleSearchBar() {
mutableStateFlow.update {
it.copy(
isSearchBarActive = !state.isSearchBarActive,
)
}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write !it.isFilterAcitve and !it.isSearchBarActive instead of using state.

Comment on lines 84 to 94
private fun handleCardClicked(index : Int){
mutableStateFlow.update {
it.copy(
isCardActive = !state.isCardActive,
currentlyActiveIndex = index,
)
}

}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants