Skip to content

Commit e85c04b

Browse files
authored
Merge branch 'development' into feature/loan-transactions-export-dialog
2 parents eeec2f1 + 25b0b56 commit e85c04b

3 files changed

Lines changed: 224 additions & 8 deletions

File tree

feature/client/src/commonMain/composeResources/values/strings.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,13 @@
648648
<string name="feature_fixed_deposit_interest_empty_date">Date Not Found</string>
649649
<string name="feature_fixed_account_created_successfully">Share account created successfully</string>
650650

651-
651+
<!-- Filters -->
652+
<string name="feature_client_filters">Filters</string>
653+
<string name="feature_client_account_status">Account Status</string>
654+
<string name="feature_client_status_active">Active</string>
655+
<string name="feature_client_status_closed">Closed</string>
656+
<string name="feature_client_status_overpaid">Overpaid</string>
657+
<string name="feature_client_status_waiting">Waiting for Disbursal</string>
658+
<string name="feature_client_status_pending">Pending Approval</string>
652659

653660
</resources>

feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientLoanAccounts/ClientLoanAccountsScreen.kt

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,55 @@ package com.mifos.feature.client.clientLoanAccounts
1212
import androidclient.feature.client.generated.resources.Res
1313
import androidclient.feature.client.generated.resources.cash_bundel
1414
import androidclient.feature.client.generated.resources.client_savings_item
15+
import androidclient.feature.client.generated.resources.feature_client_account_status
1516
import androidclient.feature.client.generated.resources.feature_client_dialog_action_ok
17+
import androidclient.feature.client.generated.resources.feature_client_filters
1618
import androidclient.feature.client.generated.resources.feature_client_loan_account
19+
import androidclient.feature.client.generated.resources.feature_client_status_active
20+
import androidclient.feature.client.generated.resources.feature_client_status_closed
21+
import androidclient.feature.client.generated.resources.feature_client_status_overpaid
22+
import androidclient.feature.client.generated.resources.feature_client_status_pending
1723
import androidclient.feature.client.generated.resources.filter
1824
import androidclient.feature.client.generated.resources.search
1925
import androidclient.feature.client.generated.resources.wallet
26+
import androidx.compose.foundation.background
27+
import androidx.compose.foundation.layout.Arrangement
28+
import androidx.compose.foundation.layout.Box
2029
import androidx.compose.foundation.layout.Column
2130
import androidx.compose.foundation.layout.Row
2231
import androidx.compose.foundation.layout.Spacer
2332
import androidx.compose.foundation.layout.fillMaxSize
2433
import androidx.compose.foundation.layout.fillMaxWidth
2534
import androidx.compose.foundation.layout.height
2635
import androidx.compose.foundation.layout.padding
36+
import androidx.compose.foundation.layout.size
37+
import androidx.compose.foundation.layout.width
38+
import androidx.compose.foundation.layout.wrapContentSize
2739
import androidx.compose.foundation.lazy.LazyColumn
2840
import androidx.compose.foundation.lazy.items
41+
import androidx.compose.foundation.shape.CircleShape
2942
import androidx.compose.material3.AlertDialog
43+
import androidx.compose.material3.Checkbox
3044
import androidx.compose.material3.ExperimentalMaterial3Api
45+
import androidx.compose.material3.HorizontalDivider
3146
import androidx.compose.material3.Icon
3247
import androidx.compose.material3.IconButton
48+
import androidx.compose.material3.MaterialTheme
49+
import androidx.compose.material3.ModalBottomSheet
50+
import androidx.compose.material3.SheetState
3351
import androidx.compose.material3.Text
3452
import androidx.compose.material3.TextButton
53+
import androidx.compose.material3.rememberModalBottomSheetState
3554
import androidx.compose.runtime.Composable
3655
import androidx.compose.runtime.getValue
3756
import androidx.compose.runtime.remember
57+
import androidx.compose.ui.Alignment
3858
import androidx.compose.ui.Modifier
59+
import androidx.compose.ui.draw.clip
3960
import androidx.compose.ui.unit.dp
4061
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4162
import androidx.navigation.NavController
63+
import com.mifos.core.designsystem.icon.MifosIcons
4264
import com.mifos.core.designsystem.theme.DesignToken
4365
import com.mifos.core.designsystem.theme.MifosTypography
4466
import com.mifos.core.ui.components.Actions
@@ -53,6 +75,7 @@ import org.jetbrains.compose.resources.stringResource
5375
import org.jetbrains.compose.resources.vectorResource
5476
import org.koin.compose.viewmodel.koinViewModel
5577

78+
@OptIn(ExperimentalMaterial3Api::class)
5679
@Composable
5780
internal fun ClientLoanAccountsScreenRoute(
5881
navigateBack: () -> Unit,
@@ -62,6 +85,7 @@ internal fun ClientLoanAccountsScreenRoute(
6285
viewModel: ClientLoanAccountsViewModel = koinViewModel(),
6386
) {
6487
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
88+
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
6589

6690
EventsEffect(viewModel.eventFlow) { event ->
6791
when (event) {
@@ -81,6 +105,17 @@ internal fun ClientLoanAccountsScreenRoute(
81105
state = state,
82106
onAction = remember(viewModel) { { viewModel.trySendAction(it) } },
83107
)
108+
if (state.isFilterDialogOpen) {
109+
FilterBottomSheet(
110+
onDismissRequest = { viewModel.trySendAction(ClientLoanAccountsAction.ToggleFilter) },
111+
sheetState = sheetState,
112+
selectedStatuses = state.selectedStatus,
113+
handleFilterClick = { status ->
114+
viewModel.trySendAction(ClientLoanAccountsAction.HandleFilterClick(status))
115+
},
116+
clearFilters = { viewModel.trySendAction(ClientLoanAccountsAction.ClearFilters) },
117+
)
118+
}
84119
}
85120

86121
@Composable
@@ -106,6 +141,7 @@ private fun ClientLoanAccountsScreen(
106141
ClientsAccountHeader(
107142
totalItem = state.loanAccounts.size.toString(),
108143
onAction = onAction,
144+
isFilterActive = state.selectedStatus.isNotEmpty(),
109145
)
110146

111147
if (state.isSearchBarActive) {
@@ -183,7 +219,7 @@ private fun ClientLoanAccountsScreen(
183219
},
184220
)
185221

186-
Spacer(modifier = Modifier.height(8.dp))
222+
Spacer(modifier = Modifier.height(DesignToken.padding.small))
187223
}
188224
}
189225
}
@@ -196,6 +232,7 @@ private fun ClientLoanAccountsScreen(
196232
@Composable
197233
private fun ClientsAccountHeader(
198234
totalItem: String,
235+
isFilterActive: Boolean,
199236
onAction: (ClientLoanAccountsAction) -> Unit,
200237
) {
201238
Row(
@@ -225,13 +262,29 @@ private fun ClientsAccountHeader(
225262
)
226263
}
227264

228-
IconButton(
229-
onClick = { onAction.invoke(ClientLoanAccountsAction.ToggleFilter) },
265+
Box(
266+
modifier = Modifier.wrapContentSize(),
267+
contentAlignment = Alignment.Center,
230268
) {
231-
Icon(
232-
painter = painterResource(Res.drawable.filter),
233-
contentDescription = null,
234-
)
269+
IconButton(
270+
onClick = { onAction.invoke(ClientLoanAccountsAction.ToggleFilter) },
271+
) {
272+
Icon(
273+
painter = painterResource(Res.drawable.filter),
274+
contentDescription = null,
275+
)
276+
}
277+
278+
if (isFilterActive) {
279+
Box(
280+
modifier = Modifier
281+
.align(Alignment.TopEnd)
282+
.padding(top = 12.dp, end = 16.dp)
283+
.size(8.dp)
284+
.clip(CircleShape)
285+
.background(MaterialTheme.colorScheme.error),
286+
)
287+
}
235288
}
236289
}
237290
}
@@ -261,3 +314,90 @@ private fun ClientLoanAccountsDialog(
261314
else -> null
262315
}
263316
}
317+
318+
@OptIn(ExperimentalMaterial3Api::class)
319+
@Composable
320+
private fun FilterBottomSheet(
321+
onDismissRequest: () -> Unit,
322+
sheetState: SheetState,
323+
handleFilterClick: (LoanStatusFilter) -> Unit,
324+
selectedStatuses: Set<LoanStatusFilter>,
325+
clearFilters: () -> Unit,
326+
) {
327+
ModalBottomSheet(
328+
onDismissRequest = onDismissRequest,
329+
sheetState = sheetState,
330+
dragHandle = null,
331+
containerColor = MaterialTheme.colorScheme.background,
332+
) {
333+
Column(
334+
modifier = Modifier.padding(DesignToken.padding.large),
335+
) {
336+
Row(
337+
verticalAlignment = Alignment.CenterVertically,
338+
horizontalArrangement = Arrangement.SpaceBetween,
339+
modifier = Modifier
340+
.fillMaxWidth()
341+
.padding(DesignToken.padding.medium),
342+
) {
343+
Text(
344+
text = stringResource(Res.string.feature_client_filters),
345+
style = MifosTypography.titleLargeEmphasized,
346+
color = MaterialTheme.colorScheme.primary,
347+
)
348+
Row {
349+
IconButton(
350+
onClick = {
351+
clearFilters()
352+
onDismissRequest()
353+
},
354+
) {
355+
Icon(
356+
imageVector = MifosIcons.Redo,
357+
contentDescription = "Clear",
358+
)
359+
}
360+
361+
IconButton(
362+
onClick = onDismissRequest,
363+
) {
364+
Icon(
365+
imageVector = MifosIcons.Check,
366+
contentDescription = "Apply",
367+
)
368+
}
369+
}
370+
}
371+
}
372+
373+
HorizontalDivider(Modifier.fillMaxWidth(), thickness = 1.5.dp)
374+
Column(modifier = Modifier.padding(DesignToken.padding.medium)) {
375+
Text(
376+
text = stringResource(Res.string.feature_client_account_status),
377+
style = MifosTypography.titleMediumEmphasized,
378+
modifier = Modifier.padding(bottom = DesignToken.padding.small),
379+
)
380+
381+
LoanStatusFilter.entries.forEach { status ->
382+
val isChecked = selectedStatuses.contains(status)
383+
val statusLabel = when (status) {
384+
LoanStatusFilter.ACTIVE -> stringResource(Res.string.feature_client_status_active)
385+
LoanStatusFilter.PENDING -> stringResource(Res.string.feature_client_status_pending)
386+
LoanStatusFilter.CLOSED -> stringResource(Res.string.feature_client_status_closed)
387+
LoanStatusFilter.OVERPAID -> stringResource(Res.string.feature_client_status_overpaid)
388+
}
389+
390+
Row(
391+
verticalAlignment = Alignment.CenterVertically,
392+
) {
393+
Spacer(modifier = Modifier.width(DesignToken.padding.medium))
394+
Checkbox(
395+
checked = isChecked,
396+
onCheckedChange = { handleFilterClick(status) },
397+
)
398+
Text(text = statusLabel)
399+
}
400+
}
401+
}
402+
}
403+
}

feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientLoanAccounts/ClientLoanAccountsViewModel.kt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.mifos.core.data.repository.ClientDetailsRepository
1616
import com.mifos.core.data.util.NetworkMonitor
1717
import com.mifos.core.ui.util.BaseViewModel
1818
import com.mifos.room.entities.accounts.loans.LoanAccountEntity
19+
import com.mifos.room.entities.accounts.loans.LoanStatusEntity
1920
import kotlinx.coroutines.flow.first
2021
import kotlinx.coroutines.flow.update
2122
import kotlinx.coroutines.launch
@@ -72,6 +73,9 @@ class ClientLoanAccountsViewModel(
7273
action.loanId,
7374
),
7475
)
76+
is ClientLoanAccountsAction.HandleFilterClick -> handleFilterClick(action.status)
77+
78+
is ClientLoanAccountsAction.ClearFilters -> clearFilters()
7579

7680
ClientLoanAccountsAction.NavigateBack -> {
7781
sendEvent(ClientLoanAccountsEvent.NavigateBack)
@@ -113,6 +117,8 @@ class ClientLoanAccountsViewModel(
113117
mutableStateFlow.update {
114118
it.copy(
115119
loanAccounts = loanAccounts,
120+
unfilteredLoanAccounts = loanAccounts,
121+
selectedStatus = emptySet(),
116122
dialogState = null,
117123
isLoading = false,
118124
)
@@ -129,13 +135,67 @@ class ClientLoanAccountsViewModel(
129135
}
130136
}
131137
}
138+
139+
private fun handleFilterClick(status: LoanStatusFilter) {
140+
mutableStateFlow.update { state ->
141+
val newSelectedStatus = if (status in state.selectedStatus) {
142+
state.selectedStatus - status
143+
} else {
144+
state.selectedStatus + status
145+
}
146+
147+
val filteredList = if (newSelectedStatus.isEmpty()) {
148+
state.unfilteredLoanAccounts
149+
} else {
150+
state.unfilteredLoanAccounts.filter { loan ->
151+
val loanStatus = loan.status ?: return@filter false
152+
153+
newSelectedStatus.any { filter ->
154+
when (filter) {
155+
LoanStatusFilter.ACTIVE -> loanStatus.active == true
156+
LoanStatusFilter.CLOSED -> loanStatus.isActuallyClosed
157+
LoanStatusFilter.PENDING -> loanStatus.isPending
158+
LoanStatusFilter.OVERPAID -> loanStatus.isOverpaid
159+
}
160+
}
161+
}
162+
}
163+
164+
state.copy(
165+
selectedStatus = newSelectedStatus,
166+
loanAccounts = filteredList,
167+
)
168+
}
169+
}
170+
171+
private fun clearFilters() {
172+
mutableStateFlow.update {
173+
it.copy(
174+
loanAccounts = it.unfilteredLoanAccounts,
175+
selectedStatus = emptySet(),
176+
)
177+
}
178+
}
179+
private val LoanStatusEntity.isActuallyClosed: Boolean
180+
get() = this.closed == true ||
181+
this.closedObligationsMet == true ||
182+
this.closedRescheduled == true ||
183+
this.closedWrittenOff == true
184+
185+
private val LoanStatusEntity.isPending: Boolean
186+
get() = this.pendingApproval == true
187+
188+
private val LoanStatusEntity.isOverpaid: Boolean
189+
get() = this.overpaid == true
132190
}
133191

134192
data class ClientLoanAccountsState(
135193
val clientId: Int = -1,
136194
val isSearchBarActive: Boolean = false,
137195
val searchText: String = "",
196+
val unfilteredLoanAccounts: List<LoanAccountEntity> = emptyList(),
138197
val loanAccounts: List<LoanAccountEntity> = emptyList(),
198+
val selectedStatus: Set<LoanStatusFilter> = emptySet(),
139199
val isFilterDialogOpen: Boolean = false,
140200
val dialogState: DialogState? = null,
141201
val isLoading: Boolean = false,
@@ -162,4 +222,13 @@ sealed interface ClientLoanAccountsAction {
162222
data class UpdateSearchValue(val query: String) : ClientLoanAccountsAction
163223
data object OnSearchClick : ClientLoanAccountsAction
164224
data object CloseDialog : ClientLoanAccountsAction
225+
data class HandleFilterClick(val status: LoanStatusFilter) : ClientLoanAccountsAction
226+
data object ClearFilters : ClientLoanAccountsAction
227+
}
228+
229+
enum class LoanStatusFilter {
230+
ACTIVE,
231+
PENDING,
232+
CLOSED,
233+
OVERPAID,
165234
}

0 commit comments

Comments
 (0)