Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,152 @@
package com.mifos.feature.loan.ClientCollateral

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
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.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ClientCollateralScreen(
viewModel: ClientCollateralViewModel = koinViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

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.

Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.TopCenter // Changed to TopCenter for list display
) {
when (val state = uiState) {
is ClientCollateralUiState.Loading -> {
// Centered loading indicator
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is ClientCollateralUiState.Success -> {
CollateralList(items = state.items)
}
is ClientCollateralUiState.Empty -> {
// Centered empty state message
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EmptyCollateralState()
}
}
is ClientCollateralUiState.Error -> {
// Centered error state message
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
ErrorState(message = state.message, onRetry = { viewModel.loadCollateralItems() })
}
}
}
}
}
}

@Composable
fun CollateralList(items: List<CollateralDisplayItem>) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(top = 8.dp, bottom = 8.dp)
) {
items(items, key = { it.id }) { item -> // Use item.id as a key for better performance
CollateralListItem(item = item, onActionClick = { /* TODO: Handle action click */ })
}
}
}

@Composable
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.


@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 {


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

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



data class CollateralDisplayItem(
val id: Int,
val typeName: String,
val quantity: Int,
val unitValue: Double,
val totalCollateralValue: Double
)
sealed interface ClientCollateralUiState {
data object Loading : ClientCollateralUiState
data class Error(val message: String) : ClientCollateralUiState
data object Empty : ClientCollateralUiState
data class Success(val items: List<CollateralDisplayItem>, val totalItems: Int) : ClientCollateralUiState
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,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() {

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.

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
Contributor 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

1 change: 1 addition & 0 deletions feature/savings/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ kotlin{
implementation(compose.components.uiToolingPreview)
implementation(compose.ui)
implementation(libs.kotlinx.serialization.json)

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
accompanistPermission = "0.34.0"
androidDesugarJdkLibs = "2.1.4"
androidGradlePlugin = "8.7.3"
androidGradlePlugin = "8.12.2"
androidTools = "31.8.0"
androidxActivity = "1.10.0"
androidxAppCompat = "1.7.0"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Feb 02 11:29:16 IST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading