Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface ClientDetailsRepository {

suspend fun getClientCloseTemplate(): DataState<ClientCloseTemplateResponse>

suspend fun getCollateralItems(): DataState<List<CollateralItem>>
suspend fun getCollateralItems(clientId: Int): DataState<List<CollateralItem>>

suspend fun getClient(clientId: Int): ClientEntity

Expand Down Expand Up @@ -69,4 +69,6 @@ interface ClientDetailsRepository {
collateralId: Int,
quantity: String,
): DataState<Unit>


}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ClientDetailsRepositoryImp(
}
}

override suspend fun getCollateralItems(): DataState<List<CollateralItem>> {
override suspend fun getCollateralItems(clientId: Int): DataState<List<CollateralItem>> {
return try {
val res = dataManagerClient.getCollateralItems()
return DataState.Success(res)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import com.mifos.core.data.repository.ClientDetailsRepository
import com.mifos.core.data.util.NetworkMonitor
import com.mifos.core.network.model.CollateralItem
import com.mifos.core.ui.components.ResultStatus

import com.mifos.core.ui.util.BaseViewModel

import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

Expand All @@ -44,7 +46,7 @@ internal class ClientCollateralViewModel(

private suspend fun loadCollaterals() {
mutableStateFlow.update { it.copy(dialogState = ClientCollateralState.DialogState.Loading) }
val result = repo.getCollateralItems()
val result = repo.getCollateralItems(route.clientId)
when (result) {
is DataState.Error -> {
mutableStateFlow.update {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package com.mifos.feature.loan.ClientCollateral



import androidclient.feature.client.generated.resources.Res
import androidclient.feature.client.generated.resources.client_product_shares_account
import androidclient.feature.client.generated.resources.client_savings_item
import androidclient.feature.client.generated.resources.filter
import androidclient.feature.client.generated.resources.search
import androidclient.feature.client.generated.resources.string_not_available
import androidclient.feature.loan.generated.resources.Res
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.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
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.theme.DesignToken
import com.mifos.core.designsystem.theme.MifosTypography
import com.mifos.core.designsystem.utils.onClick
import com.mifos.core.ui.components.Actions
import com.mifos.core.ui.components.MifosActionsCollateralDataListingComponent
import com.mifos.core.ui.components.MifosActionsShareListingComponent
import com.mifos.core.ui.components.MifosBreadcrumbNavBar
import com.mifos.core.ui.components.MifosEmptyCard
import com.mifos.core.ui.util.EventsEffect
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
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.

@Composable
internal fun CollateralScreenRoute(
navController: NavController,
viewAccount: (Int) -> Unit,
viewModel: ClientCollateralViewmodel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()

EventsEffect(viewModel.eventFlow) { event ->
when (event) {
is collateralEvent.viewAccount -> viewAccount(event.accountsId)
}
}

collateralScreen(
state = state,
navController = navController,
onAction = remember(viewModel) { { viewModel.trySendAction(it) } },
)

ShareAccountsDialog(
state = state,
onAction = remember(viewModel) { { viewModel.trySendAction(it) } },
)
}

@Composable
internal fun collateralScreen(
navController: NavController,
state: collateralUiState,
onAction: (collateralAction) -> Unit,
) {
MifosScaffold(
title = "Share Accounts",
onBackPressed = {},
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues)
.fillMaxSize(),
) {
MifosBreadcrumbNavBar(
navController = navController,
)

when (state.isLoading) {
true -> MifosCircularProgress()

false -> {
Column(
modifier = Modifier.fillMaxSize()
.padding(horizontal = DesignToken.padding.large),
) {
ShareAccountHeader(
totalItem = state.accounts.size.toString(),
onAction = onAction,
)

Spacer(modifier = Modifier.height(DesignToken.padding.large))

if (state.accounts.isNotEmpty()) {
val emptyText = stringResource(Res.string.string_not_available)

LazyColumn {
item {
state.accounts.forEachIndexed { index, account ->
MifosActionsCollateralDataListingComponent(
name = account.name ?: emptyText,
quantity = account.quantity?.toString() ?: emptyText,
totalValue = account.totalValue?.toString() ?: emptyText,
totalCollateralValue = account.totalCollateralValue?.toString() ?: emptyText,
)

Spacer(Modifier.height(DesignToken.padding.small))
}
}
}
} else {
MifosEmptyCard()
}
}
}
}
}
}
}

@Composable
private fun ShareAccountHeader(
totalItem: String,
onAction: (collateralAction) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
) {
Column {
Text(
text = stringResource(Res.string.client_product_shares_account),
style = MifosTypography.titleMedium,
)

Text(
text = totalItem + " " + stringResource(Res.string.client_savings_item),
style = MifosTypography.labelMedium,
)
}

Spacer(modifier = Modifier.weight(1f))

// add a cross icon when its active, talk with design team
Icon(
modifier = Modifier.onClick { onAction.invoke(collateralAction.toggleSearchBar) },
painter = painterResource(Res.drawable.search),
contentDescription = null,
)

Icon(
modifier = Modifier.onClick { onAction.invoke(collateralAction.toggleFiler) },
painter = painterResource(Res.drawable.filter),
contentDescription = null,
)
}
}

@Composable
private fun ShareAccountsDialog(
state: collateralUiState,
onAction: (collateralAction) -> Unit,
) {
when (state.dialogState) {
is collateralUiState.DialogState.Error -> {
MifosSweetError(
message = state.dialogState.message,
onclick = { onAction.invoke(collateralAction.refresh) },
)
}

null -> {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.mifos.feature.loan.ClientCollateral

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.mifos.core.data.repository.ClientDetailsRepository
import com.mifos.core.ui.util.BaseViewModel

import com.mifos.core.network.model.CollateralItem
import com.mifos.feature.loan.Clientcollateral.clientCollateralRoute

import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch


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

init{
fetchAllCollateralAccount()
}
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
    }
}

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.

mutableStateFlow.update{
it.copy(
isLoading = false,
dialogState = collateralUiState.DialogState.Error(e.message ?: "Unknown Error")
)
}
}
}

}
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.

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.

}

data class collateralUiState(
val isLoading : Boolean = false,
val isFilterActive : Boolean = false,
val accounts : List<CollateralItem> = emptyList(),
val isSearchBarActive :Boolean = false,
val isCardActive : Boolean = false,
val currentlyActiveIndex: Int = -1,
val dialogState : DialogState? = null,){

sealed interface DialogState{
data class Error (val message : String) : DialogState
}




}
sealed interface collateralEvent{
data class viewAccount ( val accountsId : Int) : collateralEvent

}
sealed interface collateralAction{
data object toggleFiler : collateralAction
data object toggleSearchBar : collateralAction
data class cardClicked (val activeIndex : Int ): collateralAction
data class viewAccount (val accountId : Int) : collateralAction
data object refresh : collateralAction


}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.mifos.feature.loan.Clientcollateral


import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.mifos.feature.loan.ClientCollateral.CollateralScreenRoute

import kotlinx.serialization.Serializable

@Serializable
data class clientCollateralRoute(
val clientId : Int = -1,
)
fun NavGraphBuilder.clientCollateralDestination(
navController: NavController,
navigateToViewAccount: (Int) -> Unit,
) {
composable<clientCollateralRoute> {
CollateralScreenRoute(
navController = navController,
viewAccount = navigateToViewAccount,
)
}

}
fun NavController.navigatetoCollateralScreen(
clientId : Int,
){
this.navigate(
clientCollateralRoute(
clientId = clientId,
)


)
}
Loading
Loading