feat: add filtering by status
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user