diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt index 8909d2c..a07c49a 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt @@ -56,6 +56,7 @@ fun HistoryScreen( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val isFiltered = filter.direction != DirectionFilter.ALL + || filter.status != StatusFilter.ALL Scaffold( topBar = { @@ -100,25 +101,50 @@ fun HistoryScreen( .padding(horizontal = 16.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - InputChip( - selected = true, - onClick = { vm.setDirectionFilter(DirectionFilter.ALL) }, - label = { - Text( - when (filter.direction) { - DirectionFilter.OUTGOING -> "Outgoing" - else -> "Incoming" - } - ) - }, - trailingIcon = { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Clear direction filter", - modifier = Modifier.size(16.dp) - ) - } - ) + if (filter.direction != DirectionFilter.ALL) { + InputChip( + selected = true, + onClick = { vm.setDirectionFilter(DirectionFilter.ALL) }, + label = { + Text( + when (filter.direction) { + DirectionFilter.OUTGOING -> "Outgoing" + else -> "Incoming" + } + ) + }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Clear direction filter", + modifier = Modifier.size(16.dp) + ) + } + ) + } + 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) + ) + } + ) + } } } @@ -220,6 +246,10 @@ fun HistoryScreen( onDirectionSelected = { direction -> vm.setDirectionFilter(direction) dismissFilterSheet() + }, + onStatusSelected = { status -> + vm.setStatusFilter(status) + dismissFilterSheet() } ) } @@ -233,7 +263,8 @@ private fun FilterBottomSheet( currentFilter : PaymentFilter, sheetState : SheetState, onDismiss : () -> Unit, - onDirectionSelected: (DirectionFilter) -> Unit + onDirectionSelected: (DirectionFilter) -> Unit, + onStatusSelected : (StatusFilter) -> Unit ) { ModalBottomSheet( 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("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—") if (payment.extra?.tag != null) { - DetailRow("Type", payment.extra.tag.uppercase()) + DetailRow("Type", payment.extra.tag) } if (payment.extra?.comment?.isNotBlank() == true) { DetailRow("Comment", payment.extra.comment) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt index de94215..6ecf3e0 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt @@ -22,10 +22,14 @@ import kotlinx.coroutines.launch // ── Filter model ────────────────────────────────────────────────────────────── 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 StatusFilter { ALL, COMPLETED, PENDING, FAILED } + // ── ViewModel ───────────────────────────────────────────────────────────────── @@ -48,18 +52,35 @@ class HistoryViewModel( fun setDirectionFilter(direction: DirectionFilter) { _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. */ val filteredState: StateFlow = combine(_state, _filter) { s, f -> if (s !is HistoryState.Success) return@combine s - val filtered = when (f.direction) { - DirectionFilter.ALL -> s.payments - DirectionFilter.OUTGOING -> s.payments.filter { it.isOutgoing } - DirectionFilter.INCOMING -> s.payments.filter { !it.isOutgoing } - } + val filtered = s.payments + .let { list -> + when (f.direction) { + 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) }.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading) + + private var currentOffset = 0 private var loadJob: Job? = null @@ -275,6 +296,10 @@ class HistoryViewModel( companion object { 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") } }