From 5adcc3e042da4a6d2660fc8f32e265e95038e3c7 Mon Sep 17 00:00:00 2001 From: techsavvy185 Date: Sun, 7 Dec 2025 17:03:46 +0530 Subject: [PATCH 1/4] Implemented Client filter functionality --- .../clientsList/ClientListScreen.android.kt | 106 +++++-- .../client/clientsList/ClientListScreen.kt | 286 +++++++++++++++--- .../client/clientsList/ClientListViewModel.kt | 144 ++++++++- .../clientsList/ClientListScreen.desktop.kt | 2 + .../clientsList/ClientListScreen.ios.kt | 20 ++ 5 files changed, 479 insertions(+), 79 deletions(-) create mode 100644 feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt index 70ca19fa561..30ae665b6f2 100644 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt @@ -16,6 +16,7 @@ import androidclient.feature.client.generated.resources.feature_client_no_more_c import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,8 +42,19 @@ internal actual fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier, + sort: String?, + onUpdateOffices: (List) -> Unit ) { val clientPagingList = pagingFlow.collectAsLazyPagingItems() + + val items = clientPagingList.itemSnapshotList.items + if (items.isNotEmpty()) { + val offices = items.map{ it.officeName } + .distinct() + onUpdateOffices(offices) + } + + when (clientPagingList.loadState.refresh) { is LoadState.Error -> { MifosSweetError(message = stringResource(Res.string.feature_client_failed_to_fetch_clients)) { @@ -55,54 +67,84 @@ internal actual fun LazyColumnForClientListApi( is LoadState.NotLoading -> Unit } - LazyColumn( - modifier = modifier, - ) { - items( - count = clientPagingList.itemCount, - key = { index -> clientPagingList[index]?.id ?: index }, - ) { index -> - clientPagingList[index]?.let { client -> + if (sort!=null) { + val currentItems = clientPagingList.itemSnapshotList.items + + val sortedItems = when (sort) { + "Name" -> { currentItems.sortedBy { it.displayName?.lowercase() } } + "Account Number" -> { currentItems.sortedBy { it.accountNo } } + "External ID" -> { currentItems.sortedBy { it.externalId } } + else -> currentItems + } + + + LazyColumn( + modifier = modifier, + ) { + items( + items = sortedItems, + key = { client-> client.id } + ) { client -> LaunchedEffect(client.id) { fetchImage(client.id) } ClientItem( client = client, byteArray = images[client.id], - onClientClick = onClientSelect, + onClientClick = onClientSelect ) } } - - when (clientPagingList.loadState.append) { - is LoadState.Error -> { - item { - MifosSweetError(message = stringResource(Res.string.feature_client_failed_to_more_clients)) { - onRefresh() + } else { + LazyColumn( + modifier = modifier, + ) { + items( + count = clientPagingList.itemCount, + key = { index -> clientPagingList[index]?.id ?: index }, + ) { index -> + clientPagingList[index]?.let { client -> + LaunchedEffect(client.id) { + fetchImage(client.id) } + ClientItem( + client = client, + byteArray = images[client.id], + onClientClick = onClientSelect, + ) } } - is LoadState.Loading -> { - item { - MifosPagingAppendProgress() + when (clientPagingList.loadState.append) { + is LoadState.Error -> { + item { + MifosSweetError(message = stringResource(Res.string.feature_client_failed_to_more_clients)) { + onRefresh() + } + } } - } - is LoadState.NotLoading -> { - if (clientPagingList.loadState.append.endOfPaginationReached && - clientPagingList.itemCount > 0 - ) { + is LoadState.Loading -> { item { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = DesignToken.padding.extraExtraLarge) - .padding(bottom = DesignToken.padding.extraExtraLarge), - text = stringResource(Res.string.feature_client_no_more_clients_available), - style = MifosTypography.bodyMedium, - textAlign = TextAlign.Center, - ) + MifosPagingAppendProgress() + } + } + + is LoadState.NotLoading -> { + if (clientPagingList.loadState.append.endOfPaginationReached && + clientPagingList.itemCount > 0 + ) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = DesignToken.padding.extraExtraLarge) + .padding(bottom = DesignToken.padding.extraExtraLarge), + text = stringResource(Res.string.feature_client_no_more_clients_available), + style = MifosTypography.bodyMedium, + textAlign = TextAlign.Center, + ) + } } } } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt index fd1df7659da..4f7b019421c 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt @@ -24,15 +24,26 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.RadioButton +import androidx.compose.material3.SheetState import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState 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.BiasAbsoluteAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.PagingData import com.mifos.core.designsystem.component.BasicDialogState @@ -40,6 +51,7 @@ import com.mifos.core.designsystem.component.MifosBasicDialog import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.AppColors import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTheme import com.mifos.core.designsystem.theme.MifosTypography import com.mifos.core.ui.components.MifosEmptyCard import com.mifos.core.ui.components.MifosProgressIndicator @@ -51,6 +63,7 @@ import kotlinx.coroutines.flow.Flow import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ClientListScreen( createNewClient: () -> Unit, @@ -58,8 +71,26 @@ internal fun ClientListScreen( modifier: Modifier = Modifier, viewModel: ClientListViewModel = koinViewModel(), ) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val state by viewModel.stateFlow.collectAsStateWithLifecycle() + if (state.isFilterVisible) { + FilterBottomSheet( + onDismissRequest = { viewModel.trySendAction(ClientListAction.ToggleFilterVisibility) }, + sheetState = sheetState, + removeStatus = { viewModel.trySendAction(ClientListAction.RemoveStatus(it)) }, + addStatus = { viewModel.trySendAction(ClientListAction.AddStatus(it)) }, + selectedStatuses = state.selectedStatus, + selectedSort = state.sort, + handleSortClick = { viewModel.trySendAction(ClientListAction.HandleSortClick(it)) }, + officeNames = state.officeNames, + selectedOffices = state.selectedOffices, + addOffice = { viewModel.trySendAction(ClientListAction.AddOffice(it)) }, + removeOffice = { viewModel.trySendAction(ClientListAction.RemoveOffice(it)) }, + clearFilters = { viewModel.trySendAction(ClientListAction.ClearFilters) } + ) + } + EventsEffect(viewModel.eventFlow) { event -> when (event) { is ClientListEvent.OnClientClick -> onClientClick(event.clientId) @@ -71,6 +102,8 @@ internal fun ClientListScreen( modifier = modifier, state = state, onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + toggleFilterVisibility = { viewModel.trySendAction(ClientListAction.ToggleFilterVisibility) }, + onUpdateOffices = { viewModel.trySendAction(ClientListAction.OnUpdateOffice(it)) } ) ClientListDialogs( @@ -86,6 +119,7 @@ private fun ClientActions( state: ClientListState, onAction: (ClientListAction) -> Unit, modifier: Modifier = Modifier, + toggleFilterVisibility: () -> Unit ) { Row( modifier = modifier.fillMaxWidth().padding(DesignToken.padding.large), @@ -143,14 +177,15 @@ private fun ClientActions( // } } Spacer(Modifier.width(DesignToken.padding.largeIncreased)) -// Icon( -// imageVector = MifosIcons.Filter, -// contentDescription = null, -// modifier = Modifier -// .size(DesignToken.sizes.iconAverage) -// .clickable { -// }, -// ) + Icon( + imageVector = MifosIcons.Filter, + contentDescription = null, + modifier = Modifier + .size(DesignToken.sizes.iconAverage) + .clickable { + toggleFilterVisibility() + }, + ) } } @@ -159,47 +194,55 @@ private fun ClientListContentScreen( state: ClientListState, modifier: Modifier = Modifier, onAction: (ClientListAction) -> Unit, + toggleFilterVisibility: () -> Unit, + onUpdateOffices: (List) -> Unit ) { - if (state.isEmpty) { - MifosEmptyCard("No clients found") - } - if (state.clients.isNotEmpty()) { - ClientListContent( - clientsList = state.clients, - onClientClick = { clientId -> - onAction(ClientListAction.OnClientClick(clientId)) - }, - modifier = modifier.padding(DesignToken.padding.large), - fetchImage = { - onAction(ClientListAction.FetchImage(it)) - }, - images = state.clientImages, - ) - } - if (state.clientsFlow != null) { - Column( - Modifier.fillMaxSize(), - ) { - if (state.dialogState == null) { - ClientActions( - state = state, - onAction = onAction, + Column( + modifier = Modifier.fillMaxSize() + ){ + if (!state.isEmpty) { + ClientActions( + state = state, + onAction = onAction, + toggleFilterVisibility = toggleFilterVisibility, + ) + } + + when { + state.clients.isNotEmpty() -> { + ClientListContent( + clientsList = state.clients, + onClientClick = { clientId -> + onAction(ClientListAction.OnClientClick(clientId)) + }, + modifier = modifier.padding(DesignToken.padding.large), + fetchImage = { + onAction(ClientListAction.FetchImage(it)) + }, + images = state.clientImages, ) } - LazyColumnForClientListApi( - pagingFlow = state.clientsFlow, - onRefresh = { - onAction(ClientListAction.RefreshClients) - }, - onClientSelect = { - onAction(ClientListAction.OnClientClick(it)) - }, - modifier = Modifier, - fetchImage = { - onAction(ClientListAction.FetchImage(it)) - }, - images = state.clientImages, - ) + state.clientsFlow != null -> { + LazyColumnForClientListApi( + pagingFlow = state.clientsFlow, + onRefresh = { + onAction(ClientListAction.RefreshClients) + }, + onClientSelect = { + onAction(ClientListAction.OnClientClick(it)) + }, + modifier = Modifier, + fetchImage = { + onAction(ClientListAction.FetchImage(it)) + }, + images = state.clientImages, + sort = state.sort, + onUpdateOffices = onUpdateOffices + ) + } + else -> { + MifosEmptyCard("No clients found") + } } } } @@ -310,4 +353,155 @@ internal expect fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier = Modifier, + sort: String?, + onUpdateOffices: (List) -> Unit ) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterBottomSheet( + onDismissRequest: () -> Unit, + sheetState: SheetState, + removeStatus: (String) -> Unit, + addStatus: (String) -> Unit, + selectedStatuses: List, + selectedSort: String?, + handleSortClick: (String) -> Unit, + officeNames: List, + selectedOffices: List, + addOffice: (String) -> Unit, + removeOffice: (String) -> Unit, + clearFilters: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + dragHandle = null, + containerColor = MaterialTheme.colorScheme.background + ) { + val sortTypes = listOf("Name", "Account Number", "External ID") + val statusTypes = listOf("Active", "Pending", "Closed") + + Column( + modifier = Modifier.padding(15.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + .padding(10.dp) + ) { + Text( + text = "Filters", + style = MifosTypography.titleLargeEmphasized, + color = MaterialTheme.colorScheme.primary, + ) + Row{ + IconButton( + onClick = { + clearFilters() + onDismissRequest() + }, + ) { + Icon( + imageVector = MifosIcons.Redo, + contentDescription = "Clear", + ) + } + IconButton( + onClick = onDismissRequest, + ) { + Icon( + imageVector = MifosIcons.Check, + contentDescription = "Apply", + ) + } + } + } + HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) + Column( + modifier = Modifier.padding(10.dp) + ){ + Text( + text = "Sort by", + style = MifosTypography.titleMediumEmphasized, + ) + sortTypes.forEach { sort -> + val isSelected = (sort == selectedSort) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(10.dp)) + RadioButton( + selected = isSelected, + onClick = { + handleSortClick(sort) + }, + ) + Text(text = sort) + } + } + } + HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) + Column( + modifier = Modifier.padding(10.dp), + ){ + Text( + text = "Account Status", + style = MifosTypography.titleMediumEmphasized, + ) + statusTypes.forEach { status -> + val isChecked = selectedStatuses.contains(status) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = isChecked, + onCheckedChange = { + if (it) { + addStatus(status) + } else { + removeStatus(status) + } + }, + ) + Text(text = status) + } + } + } + HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) + + Column( + modifier = Modifier.padding(10.dp) + ){ + Text( + "Office Name", + style = MifosTypography.titleMediumEmphasized + ) + officeNames.forEach { name -> + val isChecked = selectedOffices.contains(name) + if (name != null) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = isChecked, + onCheckedChange = { + if (it) { + addOffice(name) + } else { + removeOffice(name) + } + }, + ) + Text(text = name) + } + } + } + } + HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) + } + } +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt index f31365f56e4..6c8003b3907 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt @@ -13,6 +13,7 @@ import androidclient.feature.client.generated.resources.Res import androidclient.feature.client.generated.resources.feature_client_failed_to_load_client import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import androidx.paging.filter import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.Page import com.mifos.core.data.repository.ClientDetailsRepository @@ -21,8 +22,11 @@ import com.mifos.core.datastore.UserPreferencesRepository import com.mifos.core.ui.util.BaseViewModel import com.mifos.core.ui.util.imageToByteArray import com.mifos.room.entities.client.ClientEntity +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -36,6 +40,7 @@ internal class ClientListViewModel( clients = emptyList(), isOnline = false, clientsFlow = null, + unfilteredClients = emptyList() ), ) { @@ -74,6 +79,15 @@ internal class ClientListViewModel( ) } } + + ClientListAction.ToggleFilterVisibility -> toggleFilterVisibility() + is ClientListAction.AddStatus -> addStatus(action.status) + is ClientListAction.RemoveStatus -> removeStatus(action.status) + is ClientListAction.AddOffice -> addOffice(action.office) + is ClientListAction.RemoveOffice -> removeOffice(action.office) + is ClientListAction.HandleSortClick -> handleSortClick(action.sort) + is ClientListAction.OnUpdateOffice -> onUpdateOffice(action.offices) + ClientListAction.ClearFilters -> clearFilters() } } @@ -144,7 +158,7 @@ internal class ClientListViewModel( if (data.isEmpty()) { it.copy(isEmpty = true, dialogState = null) } else { - it.copy(clients = data, dialogState = null) + it.copy(clients = data, dialogState = null, unfilteredClients = data) } } } @@ -155,6 +169,7 @@ internal class ClientListViewModel( state.copy( clientsFlow = result, dialogState = null, + unfilteredClientsFlow = result ) } } @@ -177,6 +192,117 @@ internal class ClientListViewModel( } } } + + private fun addStatus(status: String) { + updateState { + val newSelectedStatus = it.selectedStatus + status + it.copy( + selectedStatus = newSelectedStatus, + ) + } + applyFilters() + } + + private fun removeStatus(status: String) { + updateState { + val newSelectedStatus = it.selectedStatus - status + it.copy( + selectedStatus = newSelectedStatus + ) + } + applyFilters() + } + + private fun handleSortClick(sort: String?) { + updateState { + val sortedList = when (sort) { + "Name" -> it.clients.sortedBy{ it.displayName?.lowercase() } + "Account Number" -> it.clients.sortedBy { it.accountNo } + "External ID" -> it.clients.sortedBy { it.externalId} + else -> it.clients + } + + it.copy( + sort = sort, + clients = sortedList + ) + } + } + + + private fun toggleFilterVisibility() { + updateState { + it.copy( + isFilterVisible = !it.isFilterVisible + ) + } + } + + private fun onUpdateOffice(offices: List) { + updateState { + it.copy( + officeNames = (offices + it.officeNames).distinct().sortedBy { it } + ) + } + } + + private fun addOffice(office: String) { + updateState { + val newSelectedOffices = it.selectedOffices + office + it.copy( + selectedOffices = newSelectedOffices + ) + } + applyFilters() + } + + private fun removeOffice(office: String) { + updateState { + val newSelectedOffices = it.selectedOffices - office + it.copy( + selectedOffices = newSelectedOffices, + ) + } + applyFilters() + } + + private fun applyFilters() { + + fun keep(client: ClientEntity): Boolean { + val statusMatch = state.selectedStatus.isEmpty() || client.status?.value in state.selectedStatus + val officeMatch = state.selectedOffices.isEmpty() || (client.officeName?: "Null") in state.selectedOffices + + return statusMatch && officeMatch + } + val filteredList = state.unfilteredClients.filter { client -> + keep(client) + } + + val filteredFlow = state.unfilteredClientsFlow?.map { clients -> + clients.filter { client-> + keep(client) + } + } + + updateState { + it.copy( + clients = filteredList, + clientsFlow = filteredFlow + ) + } + } + + private fun clearFilters() { + updateState { + it.copy( + clients = it.unfilteredClients, + clientsFlow = it.unfilteredClientsFlow, + sort = null, + selectedStatus = emptyList(), + selectedOffices = emptyList() + ) + } + } } /** @@ -184,13 +310,20 @@ internal class ClientListViewModel( */ data class ClientListState( val clients: List, + val unfilteredClients: List, val clientsFlow: Flow>?, + val unfilteredClientsFlow: Flow>? = null, val isOnline: Boolean, val isEmpty: Boolean = false, val isSearchActive: Boolean = false, val dialogState: DialogState? = null, val searchQuery: String = "", val clientImages: Map = emptyMap(), + val sort: String? = null, + val selectedStatus: List = emptyList(), + val isFilterVisible: Boolean = false, + val officeNames: List = emptyList(), + val selectedOffices: List = emptyList() ) { sealed interface DialogState { data class Error(val message: String) : DialogState @@ -218,6 +351,15 @@ sealed interface ClientListAction { data object DismissSearch : ClientListAction data object NavigateToCreateClient : ClientListAction data class OnQueryChange(val query: String) : ClientListAction + data object ToggleFilterVisibility : ClientListAction + data class AddStatus(val status: String) : ClientListAction + data class RemoveStatus(val status: String) : ClientListAction + data class AddOffice(val office: String) : ClientListAction + data class RemoveOffice(val office: String) : ClientListAction + data class HandleSortClick(val sort: String) : ClientListAction + data class OnUpdateOffice(val offices: List) : ClientListAction + data object ClearFilters : ClientListAction + sealed class Internal : ClientListAction { data class ReceiveClientResult(val result: Flow>) : Internal() diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt index ccb46e1edf3..7724f8fc6d2 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt @@ -23,5 +23,7 @@ internal actual fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier, + sort: String?, + onUpdateOffices: (List) -> Unit ) { } diff --git a/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt new file mode 100644 index 00000000000..6506525e03e --- /dev/null +++ b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt @@ -0,0 +1,20 @@ +package com.mifos.feature.client.clientsList + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.paging.PagingData +import com.mifos.room.entities.client.ClientEntity +import kotlinx.coroutines.flow.Flow + +@Composable +internal actual fun LazyColumnForClientListApi( + pagingFlow: Flow>, + onRefresh: () -> Unit, + onClientSelect: (Int) -> Unit, + fetchImage: (Int) -> Unit, + images: Map, + modifier: Modifier, + sort: String?, + onUpdateOffices: (List) -> Unit +) { +} \ No newline at end of file From 25581691efb1eb9161fc18a4ce8cba803e2a10ed Mon Sep 17 00:00:00 2001 From: techsavvy185 Date: Sun, 7 Dec 2025 17:50:44 +0530 Subject: [PATCH 2/4] Implemented Client filter functionality --- .../client/clientsList/ClientListScreen.kt | 198 +++++++++++++----- 1 file changed, 140 insertions(+), 58 deletions(-) diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt index 4f7b019421c..276293182d8 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt @@ -12,6 +12,7 @@ package com.mifos.feature.client.clientsList import androidclient.feature.client.generated.resources.Res import androidclient.feature.client.generated.resources.account_number_prefix import androidclient.feature.client.generated.resources.string_not_available +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -38,7 +39,9 @@ import androidx.compose.material3.rememberModalBottomSheetState 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAbsoluteAlignment import androidx.compose.ui.Modifier @@ -422,23 +425,49 @@ fun FilterBottomSheet( Column( modifier = Modifier.padding(10.dp) ){ - Text( - text = "Sort by", - style = MifosTypography.titleMediumEmphasized, - ) - sortTypes.forEach { sort -> - val isSelected = (sort == selectedSort) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.width(10.dp)) - RadioButton( - selected = isSelected, - onClick = { - handleSortClick(sort) - }, + var isExpanded by remember { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth() + .clickable(onClick = { + isExpanded = !isExpanded + }), + horizontalArrangement = Arrangement.SpaceBetween + ){ + Text( + text = "Sort by", + style = MifosTypography.titleMediumEmphasized, + ) + if (isExpanded) { + Icon( + imageVector = MifosIcons.ArrowDropUp, + contentDescription = "" + ) + } else { + Icon( + imageVector = MifosIcons.ArrowDropDown, + contentDescription = "" ) - Text(text = sort) + } + } + AnimatedVisibility( + visible = isExpanded + ){ + Column { + sortTypes.forEach { sort -> + val isSelected = (sort == selectedSort) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(10.dp)) + RadioButton( + selected = isSelected, + onClick = { + handleSortClick(sort) + }, + ) + Text(text = sort) + } + } } } } @@ -446,27 +475,53 @@ fun FilterBottomSheet( Column( modifier = Modifier.padding(10.dp), ){ - Text( - text = "Account Status", - style = MifosTypography.titleMediumEmphasized, - ) - statusTypes.forEach { status -> - val isChecked = selectedStatuses.contains(status) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.width(10.dp)) - Checkbox( - checked = isChecked, - onCheckedChange = { - if (it) { - addStatus(status) - } else { - removeStatus(status) - } - }, + var isExpanded by remember { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth() + .clickable(onClick = { + isExpanded = !isExpanded + }), + horizontalArrangement = Arrangement.SpaceBetween + ){ + Text( + text = "Account Status", + style = MifosTypography.titleMediumEmphasized, + ) + if (isExpanded) { + Icon( + imageVector = MifosIcons.ArrowDropUp, + contentDescription = "" ) - Text(text = status) + } else { + Icon( + imageVector = MifosIcons.ArrowDropDown, + contentDescription = "" + ) + } + } + AnimatedVisibility( + visible = isExpanded + ){ + Column { + statusTypes.forEach { status -> + val isChecked = selectedStatuses.contains(status) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = isChecked, + onCheckedChange = { + if (it) { + addStatus(status) + } else { + removeStatus(status) + } + }, + ) + Text(text = status) + } + } } } } @@ -475,28 +530,55 @@ fun FilterBottomSheet( Column( modifier = Modifier.padding(10.dp) ){ - Text( - "Office Name", - style = MifosTypography.titleMediumEmphasized - ) - officeNames.forEach { name -> - val isChecked = selectedOffices.contains(name) - if (name != null) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.width(10.dp)) - Checkbox( - checked = isChecked, - onCheckedChange = { - if (it) { - addOffice(name) - } else { - removeOffice(name) - } - }, - ) - Text(text = name) + var isExpanded by remember { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth() + .clickable(onClick = { + isExpanded = !isExpanded + }), + horizontalArrangement = Arrangement.SpaceBetween + ){ + Text( + "Office Name", + style = MifosTypography.titleMediumEmphasized + ) + if (isExpanded) { + Icon( + imageVector = MifosIcons.ArrowDropUp, + contentDescription = "" + ) + } else { + Icon( + imageVector = MifosIcons.ArrowDropDown, + contentDescription = "" + ) + } + } + + AnimatedVisibility( + visible = isExpanded + ){ + Column { + officeNames.forEach { name -> + val isChecked = selectedOffices.contains(name) + if (name != null) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = isChecked, + onCheckedChange = { + if (it) { + addOffice(name) + } else { + removeOffice(name) + } + }, + ) + Text(text = name) + } + } } } } From aa8ac5b743e582fa35ade1b26099ce6bd84005e7 Mon Sep 17 00:00:00 2001 From: techsavvy185 Date: Mon, 8 Dec 2025 22:20:18 +0530 Subject: [PATCH 3/4] Implemented Client filter functionality --- .../clientsList/ClientListScreen.android.kt | 24 +++--- .../client/clientsList/ClientListScreen.kt | 81 +++++++++---------- .../client/clientsList/ClientListViewModel.kt | 32 ++++---- .../clientsList/ClientListScreen.desktop.kt | 2 +- .../clientsList/ClientListScreen.ios.kt | 13 ++- 5 files changed, 79 insertions(+), 73 deletions(-) diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt index 30ae665b6f2..bece6131d7e 100644 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt @@ -43,18 +43,17 @@ internal actual fun LazyColumnForClientListApi( images: Map, modifier: Modifier, sort: String?, - onUpdateOffices: (List) -> Unit + onUpdateOffices: (List) -> Unit, ) { val clientPagingList = pagingFlow.collectAsLazyPagingItems() val items = clientPagingList.itemSnapshotList.items if (items.isNotEmpty()) { - val offices = items.map{ it.officeName } + val offices = items.map { it.officeName } .distinct() onUpdateOffices(offices) } - when (clientPagingList.loadState.refresh) { is LoadState.Error -> { MifosSweetError(message = stringResource(Res.string.feature_client_failed_to_fetch_clients)) { @@ -67,23 +66,28 @@ internal actual fun LazyColumnForClientListApi( is LoadState.NotLoading -> Unit } - if (sort!=null) { + if (sort != null) { val currentItems = clientPagingList.itemSnapshotList.items val sortedItems = when (sort) { - "Name" -> { currentItems.sortedBy { it.displayName?.lowercase() } } - "Account Number" -> { currentItems.sortedBy { it.accountNo } } - "External ID" -> { currentItems.sortedBy { it.externalId } } + "Name" -> { + currentItems.sortedBy { it.displayName?.lowercase() } + } + "Account Number" -> { + currentItems.sortedBy { it.accountNo } + } + "External ID" -> { + currentItems.sortedBy { it.externalId } + } else -> currentItems } - LazyColumn( modifier = modifier, ) { items( items = sortedItems, - key = { client-> client.id } + key = { client -> client.id }, ) { client -> LaunchedEffect(client.id) { fetchImage(client.id) @@ -91,7 +95,7 @@ internal actual fun LazyColumnForClientListApi( ClientItem( client = client, byteArray = images[client.id], - onClientClick = onClientSelect + onClientClick = onClientSelect, ) } } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt index 276293182d8..7878df34cb3 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt @@ -43,10 +43,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAbsoluteAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.PagingData import com.mifos.core.designsystem.component.BasicDialogState @@ -54,7 +52,6 @@ import com.mifos.core.designsystem.component.MifosBasicDialog import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.AppColors import com.mifos.core.designsystem.theme.DesignToken -import com.mifos.core.designsystem.theme.MifosTheme import com.mifos.core.designsystem.theme.MifosTypography import com.mifos.core.ui.components.MifosEmptyCard import com.mifos.core.ui.components.MifosProgressIndicator @@ -90,7 +87,7 @@ internal fun ClientListScreen( selectedOffices = state.selectedOffices, addOffice = { viewModel.trySendAction(ClientListAction.AddOffice(it)) }, removeOffice = { viewModel.trySendAction(ClientListAction.RemoveOffice(it)) }, - clearFilters = { viewModel.trySendAction(ClientListAction.ClearFilters) } + clearFilters = { viewModel.trySendAction(ClientListAction.ClearFilters) }, ) } @@ -106,7 +103,7 @@ internal fun ClientListScreen( state = state, onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, toggleFilterVisibility = { viewModel.trySendAction(ClientListAction.ToggleFilterVisibility) }, - onUpdateOffices = { viewModel.trySendAction(ClientListAction.OnUpdateOffice(it)) } + onUpdateOffices = { viewModel.trySendAction(ClientListAction.OnUpdateOffice(it)) }, ) ClientListDialogs( @@ -122,7 +119,7 @@ private fun ClientActions( state: ClientListState, onAction: (ClientListAction) -> Unit, modifier: Modifier = Modifier, - toggleFilterVisibility: () -> Unit + toggleFilterVisibility: () -> Unit, ) { Row( modifier = modifier.fillMaxWidth().padding(DesignToken.padding.large), @@ -198,11 +195,11 @@ private fun ClientListContentScreen( modifier: Modifier = Modifier, onAction: (ClientListAction) -> Unit, toggleFilterVisibility: () -> Unit, - onUpdateOffices: (List) -> Unit + onUpdateOffices: (List) -> Unit, ) { Column( - modifier = Modifier.fillMaxSize() - ){ + modifier = Modifier.fillMaxSize(), + ) { if (!state.isEmpty) { ClientActions( state = state, @@ -211,7 +208,7 @@ private fun ClientListContentScreen( ) } - when { + when { state.clients.isNotEmpty() -> { ClientListContent( clientsList = state.clients, @@ -240,7 +237,7 @@ private fun ClientListContentScreen( }, images = state.clientImages, sort = state.sort, - onUpdateOffices = onUpdateOffices + onUpdateOffices = onUpdateOffices, ) } else -> { @@ -357,7 +354,7 @@ internal expect fun LazyColumnForClientListApi( images: Map, modifier: Modifier = Modifier, sort: String?, - onUpdateOffices: (List) -> Unit + onUpdateOffices: (List) -> Unit, ) @OptIn(ExperimentalMaterial3Api::class) @@ -374,32 +371,32 @@ fun FilterBottomSheet( selectedOffices: List, addOffice: (String) -> Unit, removeOffice: (String) -> Unit, - clearFilters: () -> Unit + clearFilters: () -> Unit, ) { ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, dragHandle = null, - containerColor = MaterialTheme.colorScheme.background + containerColor = MaterialTheme.colorScheme.background, ) { val sortTypes = listOf("Name", "Account Number", "External ID") val statusTypes = listOf("Active", "Pending", "Closed") Column( - modifier = Modifier.padding(15.dp) + modifier = Modifier.padding(15.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() - .padding(10.dp) + .padding(10.dp), ) { Text( text = "Filters", style = MifosTypography.titleLargeEmphasized, color = MaterialTheme.colorScheme.primary, ) - Row{ + Row { IconButton( onClick = { clearFilters() @@ -423,16 +420,16 @@ fun FilterBottomSheet( } HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) Column( - modifier = Modifier.padding(10.dp) - ){ + modifier = Modifier.padding(10.dp), + ) { var isExpanded by remember { mutableStateOf(false) } Row( modifier = Modifier.fillMaxWidth() .clickable(onClick = { isExpanded = !isExpanded }), - horizontalArrangement = Arrangement.SpaceBetween - ){ + horizontalArrangement = Arrangement.SpaceBetween, + ) { Text( text = "Sort by", style = MifosTypography.titleMediumEmphasized, @@ -440,18 +437,18 @@ fun FilterBottomSheet( if (isExpanded) { Icon( imageVector = MifosIcons.ArrowDropUp, - contentDescription = "" + contentDescription = "", ) } else { Icon( imageVector = MifosIcons.ArrowDropDown, - contentDescription = "" + contentDescription = "", ) } } AnimatedVisibility( - visible = isExpanded - ){ + visible = isExpanded, + ) { Column { sortTypes.forEach { sort -> val isSelected = (sort == selectedSort) @@ -474,15 +471,15 @@ fun FilterBottomSheet( HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) Column( modifier = Modifier.padding(10.dp), - ){ + ) { var isExpanded by remember { mutableStateOf(false) } Row( modifier = Modifier.fillMaxWidth() .clickable(onClick = { isExpanded = !isExpanded }), - horizontalArrangement = Arrangement.SpaceBetween - ){ + horizontalArrangement = Arrangement.SpaceBetween, + ) { Text( text = "Account Status", style = MifosTypography.titleMediumEmphasized, @@ -490,18 +487,18 @@ fun FilterBottomSheet( if (isExpanded) { Icon( imageVector = MifosIcons.ArrowDropUp, - contentDescription = "" + contentDescription = "", ) } else { Icon( imageVector = MifosIcons.ArrowDropDown, - contentDescription = "" + contentDescription = "", ) } } AnimatedVisibility( - visible = isExpanded - ){ + visible = isExpanded, + ) { Column { statusTypes.forEach { status -> val isChecked = selectedStatuses.contains(status) @@ -528,42 +525,42 @@ fun FilterBottomSheet( HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp) Column( - modifier = Modifier.padding(10.dp) - ){ + modifier = Modifier.padding(10.dp), + ) { var isExpanded by remember { mutableStateOf(false) } Row( modifier = Modifier.fillMaxWidth() .clickable(onClick = { isExpanded = !isExpanded }), - horizontalArrangement = Arrangement.SpaceBetween - ){ + horizontalArrangement = Arrangement.SpaceBetween, + ) { Text( "Office Name", - style = MifosTypography.titleMediumEmphasized + style = MifosTypography.titleMediumEmphasized, ) if (isExpanded) { Icon( imageVector = MifosIcons.ArrowDropUp, - contentDescription = "" + contentDescription = "", ) } else { Icon( imageVector = MifosIcons.ArrowDropDown, - contentDescription = "" + contentDescription = "", ) } } AnimatedVisibility( - visible = isExpanded - ){ + visible = isExpanded, + ) { Column { officeNames.forEach { name -> val isChecked = selectedOffices.contains(name) if (name != null) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Spacer(modifier = Modifier.width(10.dp)) Checkbox( diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt index 6c8003b3907..12209fd5487 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt @@ -22,7 +22,6 @@ import com.mifos.core.datastore.UserPreferencesRepository import com.mifos.core.ui.util.BaseViewModel import com.mifos.core.ui.util.imageToByteArray import com.mifos.room.entities.client.ClientEntity -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -40,7 +39,7 @@ internal class ClientListViewModel( clients = emptyList(), isOnline = false, clientsFlow = null, - unfilteredClients = emptyList() + unfilteredClients = emptyList(), ), ) { @@ -169,7 +168,7 @@ internal class ClientListViewModel( state.copy( clientsFlow = result, dialogState = null, - unfilteredClientsFlow = result + unfilteredClientsFlow = result, ) } } @@ -207,7 +206,7 @@ internal class ClientListViewModel( updateState { val newSelectedStatus = it.selectedStatus - status it.copy( - selectedStatus = newSelectedStatus + selectedStatus = newSelectedStatus, ) } applyFilters() @@ -216,24 +215,23 @@ internal class ClientListViewModel( private fun handleSortClick(sort: String?) { updateState { val sortedList = when (sort) { - "Name" -> it.clients.sortedBy{ it.displayName?.lowercase() } + "Name" -> it.clients.sortedBy { it.displayName?.lowercase() } "Account Number" -> it.clients.sortedBy { it.accountNo } - "External ID" -> it.clients.sortedBy { it.externalId} + "External ID" -> it.clients.sortedBy { it.externalId } else -> it.clients } it.copy( sort = sort, - clients = sortedList + clients = sortedList, ) } } - private fun toggleFilterVisibility() { updateState { it.copy( - isFilterVisible = !it.isFilterVisible + isFilterVisible = !it.isFilterVisible, ) } } @@ -241,7 +239,7 @@ internal class ClientListViewModel( private fun onUpdateOffice(offices: List) { updateState { it.copy( - officeNames = (offices + it.officeNames).distinct().sortedBy { it } + officeNames = (offices + it.officeNames).distinct().sortedBy { it }, ) } } @@ -250,7 +248,7 @@ internal class ClientListViewModel( updateState { val newSelectedOffices = it.selectedOffices + office it.copy( - selectedOffices = newSelectedOffices + selectedOffices = newSelectedOffices, ) } applyFilters() @@ -267,10 +265,9 @@ internal class ClientListViewModel( } private fun applyFilters() { - fun keep(client: ClientEntity): Boolean { val statusMatch = state.selectedStatus.isEmpty() || client.status?.value in state.selectedStatus - val officeMatch = state.selectedOffices.isEmpty() || (client.officeName?: "Null") in state.selectedOffices + val officeMatch = state.selectedOffices.isEmpty() || (client.officeName ?: "Null") in state.selectedOffices return statusMatch && officeMatch } @@ -279,7 +276,7 @@ internal class ClientListViewModel( } val filteredFlow = state.unfilteredClientsFlow?.map { clients -> - clients.filter { client-> + clients.filter { client -> keep(client) } } @@ -287,7 +284,7 @@ internal class ClientListViewModel( updateState { it.copy( clients = filteredList, - clientsFlow = filteredFlow + clientsFlow = filteredFlow, ) } } @@ -299,7 +296,7 @@ internal class ClientListViewModel( clientsFlow = it.unfilteredClientsFlow, sort = null, selectedStatus = emptyList(), - selectedOffices = emptyList() + selectedOffices = emptyList(), ) } } @@ -323,7 +320,7 @@ data class ClientListState( val selectedStatus: List = emptyList(), val isFilterVisible: Boolean = false, val officeNames: List = emptyList(), - val selectedOffices: List = emptyList() + val selectedOffices: List = emptyList(), ) { sealed interface DialogState { data class Error(val message: String) : DialogState @@ -360,7 +357,6 @@ sealed interface ClientListAction { data class OnUpdateOffice(val offices: List) : ClientListAction data object ClearFilters : ClientListAction - sealed class Internal : ClientListAction { data class ReceiveClientResult(val result: Flow>) : Internal() data class ReceiveClientResultFromDb(val result: DataState>) : Internal() diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt index 7724f8fc6d2..824ef31e797 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt @@ -24,6 +24,6 @@ internal actual fun LazyColumnForClientListApi( images: Map, modifier: Modifier, sort: String?, - onUpdateOffices: (List) -> Unit + onUpdateOffices: (List) -> Unit, ) { } diff --git a/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt index 6506525e03e..824ef31e797 100644 --- a/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt +++ b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt @@ -1,3 +1,12 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ package com.mifos.feature.client.clientsList import androidx.compose.runtime.Composable @@ -15,6 +24,6 @@ internal actual fun LazyColumnForClientListApi( images: Map, modifier: Modifier, sort: String?, - onUpdateOffices: (List) -> Unit + onUpdateOffices: (List) -> Unit, ) { -} \ No newline at end of file +} From 3b3ea61a48e3bb94ebca3621b23b169e7c82f4c1 Mon Sep 17 00:00:00 2001 From: techsavvy185 Date: Thu, 11 Dec 2025 00:08:57 +0530 Subject: [PATCH 4/4] Implemented Client filter functionality --- .../clientsList/ClientListScreen.android.kt | 8 +- .../client/clientsList/ClientListScreen.kt | 38 ++---- .../client/clientsList/ClientListViewModel.kt | 117 ++++++++---------- .../clientsList/ClientListScreen.desktop.kt | 2 +- .../clientsList/ClientListScreen.ios.kt | 2 +- 5 files changed, 68 insertions(+), 99 deletions(-) diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt index bece6131d7e..fe38df6efab 100644 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.android.kt @@ -42,7 +42,7 @@ internal actual fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier, - sort: String?, + sort: SortTypes?, onUpdateOffices: (List) -> Unit, ) { val clientPagingList = pagingFlow.collectAsLazyPagingItems() @@ -70,13 +70,13 @@ internal actual fun LazyColumnForClientListApi( val currentItems = clientPagingList.itemSnapshotList.items val sortedItems = when (sort) { - "Name" -> { + SortTypes.NAME -> { currentItems.sortedBy { it.displayName?.lowercase() } } - "Account Number" -> { + SortTypes.ACCOUNT_NUMBER -> { currentItems.sortedBy { it.accountNo } } - "External ID" -> { + SortTypes.EXTERNAL_ID -> { currentItems.sortedBy { it.externalId } } else -> currentItems diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt index 7878df34cb3..0ac5b9e33c5 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.kt @@ -78,15 +78,14 @@ internal fun ClientListScreen( FilterBottomSheet( onDismissRequest = { viewModel.trySendAction(ClientListAction.ToggleFilterVisibility) }, sheetState = sheetState, - removeStatus = { viewModel.trySendAction(ClientListAction.RemoveStatus(it)) }, - addStatus = { viewModel.trySendAction(ClientListAction.AddStatus(it)) }, + handleFilterClick = { value, filterType -> + viewModel.trySendAction(ClientListAction.HandleFilterClick(value, filterType)) + }, selectedStatuses = state.selectedStatus, selectedSort = state.sort, handleSortClick = { viewModel.trySendAction(ClientListAction.HandleSortClick(it)) }, officeNames = state.officeNames, selectedOffices = state.selectedOffices, - addOffice = { viewModel.trySendAction(ClientListAction.AddOffice(it)) }, - removeOffice = { viewModel.trySendAction(ClientListAction.RemoveOffice(it)) }, clearFilters = { viewModel.trySendAction(ClientListAction.ClearFilters) }, ) } @@ -353,7 +352,7 @@ internal expect fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier = Modifier, - sort: String?, + sort: SortTypes?, onUpdateOffices: (List) -> Unit, ) @@ -362,15 +361,12 @@ internal expect fun LazyColumnForClientListApi( fun FilterBottomSheet( onDismissRequest: () -> Unit, sheetState: SheetState, - removeStatus: (String) -> Unit, - addStatus: (String) -> Unit, + handleFilterClick: (String, FilterType) -> Unit, selectedStatuses: List, - selectedSort: String?, - handleSortClick: (String) -> Unit, + selectedSort: SortTypes?, + handleSortClick: (SortTypes) -> Unit, officeNames: List, selectedOffices: List, - addOffice: (String) -> Unit, - removeOffice: (String) -> Unit, clearFilters: () -> Unit, ) { ModalBottomSheet( @@ -379,7 +375,7 @@ fun FilterBottomSheet( dragHandle = null, containerColor = MaterialTheme.colorScheme.background, ) { - val sortTypes = listOf("Name", "Account Number", "External ID") + val sortTypes = listOf(SortTypes.NAME, SortTypes.ACCOUNT_NUMBER, SortTypes.EXTERNAL_ID) val statusTypes = listOf("Active", "Pending", "Closed") Column( @@ -462,7 +458,7 @@ fun FilterBottomSheet( handleSortClick(sort) }, ) - Text(text = sort) + Text(text = sort.value) } } } @@ -508,13 +504,7 @@ fun FilterBottomSheet( Spacer(modifier = Modifier.width(10.dp)) Checkbox( checked = isChecked, - onCheckedChange = { - if (it) { - addStatus(status) - } else { - removeStatus(status) - } - }, + onCheckedChange = { handleFilterClick(status, FilterType.STATUS) }, ) Text(text = status) } @@ -565,13 +555,7 @@ fun FilterBottomSheet( Spacer(modifier = Modifier.width(10.dp)) Checkbox( checked = isChecked, - onCheckedChange = { - if (it) { - addOffice(name) - } else { - removeOffice(name) - } - }, + onCheckedChange = { handleFilterClick(name, FilterType.OFFICE) }, ) Text(text = name) } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt index 12209fd5487..7ca55c40bef 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientsList/ClientListViewModel.kt @@ -80,12 +80,9 @@ internal class ClientListViewModel( } ClientListAction.ToggleFilterVisibility -> toggleFilterVisibility() - is ClientListAction.AddStatus -> addStatus(action.status) - is ClientListAction.RemoveStatus -> removeStatus(action.status) - is ClientListAction.AddOffice -> addOffice(action.office) - is ClientListAction.RemoveOffice -> removeOffice(action.office) is ClientListAction.HandleSortClick -> handleSortClick(action.sort) is ClientListAction.OnUpdateOffice -> onUpdateOffice(action.offices) + is ClientListAction.HandleFilterClick -> handleFilterClick(action.filter, action.filterType) ClientListAction.ClearFilters -> clearFilters() } } @@ -192,32 +189,12 @@ internal class ClientListViewModel( } } - private fun addStatus(status: String) { - updateState { - val newSelectedStatus = it.selectedStatus + status - it.copy( - selectedStatus = newSelectedStatus, - ) - } - applyFilters() - } - - private fun removeStatus(status: String) { - updateState { - val newSelectedStatus = it.selectedStatus - status - it.copy( - selectedStatus = newSelectedStatus, - ) - } - applyFilters() - } - - private fun handleSortClick(sort: String?) { + private fun handleSortClick(sort: SortTypes?) { updateState { val sortedList = when (sort) { - "Name" -> it.clients.sortedBy { it.displayName?.lowercase() } - "Account Number" -> it.clients.sortedBy { it.accountNo } - "External ID" -> it.clients.sortedBy { it.externalId } + SortTypes.NAME -> it.clients.sortedBy { it.displayName?.lowercase() } + SortTypes.ACCOUNT_NUMBER -> it.clients.sortedBy { it.accountNo } + SortTypes.EXTERNAL_ID -> it.clients.sortedBy { it.externalId } else -> it.clients } @@ -244,45 +221,45 @@ internal class ClientListViewModel( } } - private fun addOffice(office: String) { - updateState { - val newSelectedOffices = it.selectedOffices + office - it.copy( - selectedOffices = newSelectedOffices, - ) - } - applyFilters() - } - - private fun removeOffice(office: String) { + private fun handleFilterClick(filter: String, filterType: FilterType) { updateState { - val newSelectedOffices = it.selectedOffices - office - it.copy( - selectedOffices = newSelectedOffices, - ) - } - applyFilters() - } - - private fun applyFilters() { - fun keep(client: ClientEntity): Boolean { - val statusMatch = state.selectedStatus.isEmpty() || client.status?.value in state.selectedStatus - val officeMatch = state.selectedOffices.isEmpty() || (client.officeName ?: "Null") in state.selectedOffices + val newSelectedStatus = if (filterType == FilterType.STATUS) { + if (filter in it.selectedStatus) { + it.selectedStatus - filter + } else { + it.selectedStatus + filter + } + } else { + it.selectedStatus + } + val newSelectedOffices = if (filterType == FilterType.OFFICE) { + if (filter in it.selectedOffices) { + it.selectedOffices - filter + } else { + it.selectedOffices + filter + } + } else { + it.selectedOffices + } - return statusMatch && officeMatch - } - val filteredList = state.unfilteredClients.filter { client -> - keep(client) - } + fun keep(client: ClientEntity): Boolean { + val statusMatch = newSelectedStatus.isEmpty() || client.status?.value in newSelectedStatus + val officeMatch = newSelectedOffices.isEmpty() || (client.officeName ?: "Null") in newSelectedOffices - val filteredFlow = state.unfilteredClientsFlow?.map { clients -> - clients.filter { client -> + return statusMatch && officeMatch + } + val filteredList = it.unfilteredClients.filter { client -> keep(client) } - } - updateState { + val filteredFlow = it.unfilteredClientsFlow?.map { clients -> + clients.filter { client -> + keep(client) + } + } it.copy( + selectedStatus = newSelectedStatus, + selectedOffices = newSelectedOffices, clients = filteredList, clientsFlow = filteredFlow, ) @@ -316,7 +293,7 @@ data class ClientListState( val dialogState: DialogState? = null, val searchQuery: String = "", val clientImages: Map = emptyMap(), - val sort: String? = null, + val sort: SortTypes? = null, val selectedStatus: List = emptyList(), val isFilterVisible: Boolean = false, val officeNames: List = emptyList(), @@ -328,6 +305,17 @@ data class ClientListState( } } +enum class SortTypes(val value: String) { + NAME("Name"), + ACCOUNT_NUMBER("Account Number"), + EXTERNAL_ID("External ID"), +} + +enum class FilterType(val value: String) { + STATUS("Status"), + OFFICE("Office"), +} + /** * UI events for the Client List screen. */ @@ -349,11 +337,8 @@ sealed interface ClientListAction { data object NavigateToCreateClient : ClientListAction data class OnQueryChange(val query: String) : ClientListAction data object ToggleFilterVisibility : ClientListAction - data class AddStatus(val status: String) : ClientListAction - data class RemoveStatus(val status: String) : ClientListAction - data class AddOffice(val office: String) : ClientListAction - data class RemoveOffice(val office: String) : ClientListAction - data class HandleSortClick(val sort: String) : ClientListAction + data class HandleFilterClick(val filter: String, val filterType: FilterType) : ClientListAction + data class HandleSortClick(val sort: SortTypes) : ClientListAction data class OnUpdateOffice(val offices: List) : ClientListAction data object ClearFilters : ClientListAction diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt index 824ef31e797..e35fe896b97 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.desktop.kt @@ -23,7 +23,7 @@ internal actual fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier, - sort: String?, + sort: SortTypes?, onUpdateOffices: (List) -> Unit, ) { } diff --git a/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt index 824ef31e797..e35fe896b97 100644 --- a/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt +++ b/feature/client/src/iosMain/kotlin/com/mifos/feature/client/clientsList/ClientListScreen.ios.kt @@ -23,7 +23,7 @@ internal actual fun LazyColumnForClientListApi( fetchImage: (Int) -> Unit, images: Map, modifier: Modifier, - sort: String?, + sort: SortTypes?, onUpdateOffices: (List) -> Unit, ) { }