feat: add filtering by status

This commit is contained in:
2026-06-02 20:50:49 +02:00
parent ecd01349ec
commit 5d118cd396
2 changed files with 106 additions and 27 deletions
@@ -56,6 +56,7 @@ fun HistoryScreen(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val isFiltered = filter.direction != DirectionFilter.ALL val isFiltered = filter.direction != DirectionFilter.ALL
|| filter.status != StatusFilter.ALL
Scaffold( Scaffold(
topBar = { topBar = {
@@ -100,6 +101,7 @@ fun HistoryScreen(
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = 16.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (filter.direction != DirectionFilter.ALL) {
InputChip( InputChip(
selected = true, selected = true,
onClick = { vm.setDirectionFilter(DirectionFilter.ALL) }, onClick = { vm.setDirectionFilter(DirectionFilter.ALL) },
@@ -120,6 +122,30 @@ fun HistoryScreen(
} }
) )
} }
if (filter.status != StatusFilter.ALL) {
InputChip(
selected = true,
onClick = { vm.setStatusFilter(StatusFilter.ALL) },
label = {
Text(
when (filter.status) {
StatusFilter.COMPLETED -> "Completed"
StatusFilter.PENDING -> "Pending"
StatusFilter.FAILED -> "Failed"
StatusFilter.ALL -> ""
}
)
},
trailingIcon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Clear status filter",
modifier = Modifier.size(16.dp)
)
}
)
}
}
} }
// ── Content ─────────────────────────────────────────────────────── // ── Content ───────────────────────────────────────────────────────
@@ -220,6 +246,10 @@ fun HistoryScreen(
onDirectionSelected = { direction -> onDirectionSelected = { direction ->
vm.setDirectionFilter(direction) vm.setDirectionFilter(direction)
dismissFilterSheet() dismissFilterSheet()
},
onStatusSelected = { status ->
vm.setStatusFilter(status)
dismissFilterSheet()
} }
) )
} }
@@ -233,7 +263,8 @@ private fun FilterBottomSheet(
currentFilter : PaymentFilter, currentFilter : PaymentFilter,
sheetState : SheetState, sheetState : SheetState,
onDismiss : () -> Unit, onDismiss : () -> Unit,
onDirectionSelected: (DirectionFilter) -> Unit onDirectionSelected: (DirectionFilter) -> Unit,
onStatusSelected : (StatusFilter) -> Unit
) { ) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -272,6 +303,29 @@ private fun FilterBottomSheet(
) )
} }
} }
Spacer(Modifier.height(20.dp))
// ── Status ───────────────────────────────────────────────────────
Text(
text = "Status",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
StatusFilter.entries.forEach { option ->
val label = when (option) {
StatusFilter.ALL -> "All"
StatusFilter.COMPLETED -> "Completed"
StatusFilter.PENDING -> "Pending"
StatusFilter.FAILED -> "Failed"
}
FilterChip(
selected = currentFilter.status == option,
onClick = { onStatusSelected(option) },
label = { Text(label) }
)
}
}
} }
} }
} }
@@ -548,7 +602,7 @@ private fun PaymentDetailContent(
DetailRow("Date", formatTimestamp(payment.time)) DetailRow("Date", formatTimestamp(payment.time))
DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "") DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "")
if (payment.extra?.tag != null) { if (payment.extra?.tag != null) {
DetailRow("Type", payment.extra.tag.uppercase()) DetailRow("Type", payment.extra.tag)
} }
if (payment.extra?.comment?.isNotBlank() == true) { if (payment.extra?.comment?.isNotBlank() == true) {
DetailRow("Comment", payment.extra.comment) DetailRow("Comment", payment.extra.comment)
@@ -22,10 +22,14 @@ import kotlinx.coroutines.launch
// ── Filter model ────────────────────────────────────────────────────────────── // ── Filter model ──────────────────────────────────────────────────────────────
data class PaymentFilter( data class PaymentFilter(
val direction: DirectionFilter = DirectionFilter.ALL val direction: DirectionFilter = DirectionFilter.ALL,
val status: StatusFilter = StatusFilter.ALL
) )
enum class DirectionFilter { ALL, OUTGOING, INCOMING } enum class DirectionFilter { ALL, OUTGOING, INCOMING }
enum class StatusFilter { ALL, COMPLETED, PENDING, FAILED }
// ── ViewModel ───────────────────────────────────────────────────────────────── // ── ViewModel ─────────────────────────────────────────────────────────────────
@@ -48,18 +52,35 @@ class HistoryViewModel(
fun setDirectionFilter(direction: DirectionFilter) { fun setDirectionFilter(direction: DirectionFilter) {
_filter.update { it.copy(direction = direction) } _filter.update { it.copy(direction = direction) }
} }
fun setStatusFilter(status: StatusFilter) {
_filter.update { it.copy(status = status) }
}
/** The list shown in the UI — raw state with filter applied. */ /** The list shown in the UI — raw state with filter applied. */
val filteredState: StateFlow<HistoryState> = combine(_state, _filter) { s, f -> val filteredState: StateFlow<HistoryState> = combine(_state, _filter) { s, f ->
if (s !is HistoryState.Success) return@combine s if (s !is HistoryState.Success) return@combine s
val filtered = when (f.direction) { val filtered = s.payments
DirectionFilter.ALL -> s.payments .let { list ->
DirectionFilter.OUTGOING -> s.payments.filter { it.isOutgoing } when (f.direction) {
DirectionFilter.INCOMING -> s.payments.filter { !it.isOutgoing } DirectionFilter.ALL -> list
DirectionFilter.OUTGOING -> list.filter { it.isOutgoing }
DirectionFilter.INCOMING -> list.filter { !it.isOutgoing }
}
}
.let { list ->
when (f.status) {
StatusFilter.ALL -> list
StatusFilter.COMPLETED -> list.filter { isCompletedStatus(it.status) }
StatusFilter.PENDING -> list.filter { isPendingStatus(it.status) }
StatusFilter.FAILED -> list.filter { isFailedStatus(it.status) }
}
} }
s.copy(payments = filtered) s.copy(payments = filtered)
}.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading) }.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading)
private var currentOffset = 0 private var currentOffset = 0
private var loadJob: Job? = null private var loadJob: Job? = null
@@ -275,6 +296,10 @@ class HistoryViewModel(
companion object { companion object {
private const val TAG = "HistoryViewModel" private const val TAG = "HistoryViewModel"
fun isCompletedStatus(s: String) = s.lowercase() in setOf("success", "complete", "paid")
fun isPendingStatus(s: String) = s.lowercase() in setOf("pending", "in_flight", "inflight")
fun isFailedStatus(s: String) = s.lowercase() in setOf("failed", "error", "expired")
} }
} }