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 4901c30..6130773 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt @@ -2,7 +2,6 @@ package com.bitcointxoko.gudariwallet.ui import android.content.ClipData import android.content.Intent -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -63,6 +62,8 @@ fun HistoryScreen( || filter.types.isNotEmpty() || filter.minAmountSat != null || filter.maxAmountSat != null + || filter.minCreatedAt != null + || filter.maxCreatedAt != null Scaffold( topBar = { @@ -187,6 +188,34 @@ fun HistoryScreen( } ) } + val minCreatedAt = filter.minCreatedAt + val maxCreatedAt = filter.maxCreatedAt + if (minCreatedAt != null || maxCreatedAt != null) { + val label = when (filter.datePreset) { + DatePreset.THIS_WEEK -> "This week" + DatePreset.THIS_MONTH -> "This month" + DatePreset.THIS_YEAR -> "This year" + null -> when { + minCreatedAt != null && maxCreatedAt != null -> + "${formatEpochShort(minCreatedAt)}–${formatEpochShort(maxCreatedAt)}" + minCreatedAt != null -> + "From ${formatEpochShort(minCreatedAt)}" + else -> + "Until ${formatEpochShort(maxCreatedAt!!)}"} + } + InputChip( + selected = true, + onClick = { vm.clearDateFilter() }, + label = { Text(label) }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Clear date filter", + modifier = Modifier.size(16.dp) + ) + } + ) + } } } @@ -292,7 +321,11 @@ fun HistoryScreen( }, onStatusToggled = { vm.toggleStatusFilter(it) }, onTypeToggled = { vm.toggleTypeFilter(it) }, - onAmountFilterSet = { min, max -> vm.setAmountFilter(min, max) } + onAmountFilterSet = { min, max -> vm.setAmountFilter(min, max) }, + onDatePresetSelected = { vm.applyDatePreset(it) }, + onDateFilterSet = { min, max -> vm.setDateFilter(min, max, preset = null) }, + onDateClearRequested = { vm.clearDateFilter() } + ) } } @@ -309,7 +342,10 @@ private fun FilterBottomSheet( onDirectionSelected: (DirectionFilter) -> Unit, onStatusToggled : (StatusFilter) -> Unit, onTypeToggled : (String) -> Unit, - onAmountFilterSet : (min: Long?, max: Long?) -> Unit + onAmountFilterSet : (min: Long?, max: Long?) -> Unit, + onDatePresetSelected: (DatePreset) -> Unit, + onDateFilterSet : (min: Long?, max: Long?) -> Unit, + onDateClearRequested: () -> Unit ) { ModalBottomSheet( onDismissRequest = onDismiss, @@ -430,6 +466,96 @@ private fun FilterBottomSheet( modifier = Modifier.weight(1f) ) } + Spacer(Modifier.height(20.dp)) + // ── Date ───────────────────────────────────────────────────────── + Text( + text = "Date", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(12.dp)) + + // Preset chips + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + DatePreset.entries.forEach { preset -> + val label = when (preset) { + DatePreset.THIS_WEEK -> "This week" + DatePreset.THIS_MONTH -> "This month" + DatePreset.THIS_YEAR -> "This year" + } + FilterChip( + selected = currentFilter.datePreset == preset, + onClick = { + if (currentFilter.datePreset == preset) onDateClearRequested() + else onDatePresetSelected(preset) + }, + label = { Text(label) } + ) + } + } + Spacer(Modifier.height(12.dp)) + + // Manual From / To date pickers + var showFromPicker by remember { mutableStateOf(false) } + var showToPicker by remember { mutableStateOf(false) } + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton( + onClick = { showFromPicker = true }, + modifier = Modifier.weight(1f) + ) { + Text(currentFilter.minCreatedAt?.let { formatEpochShort(it) } ?: "From") + } + Text("–", style = MaterialTheme.typography.bodyLarge) + OutlinedButton( + onClick = { showToPicker = true }, + modifier = Modifier.weight(1f) + ) { + Text(currentFilter.maxCreatedAt?.let { formatEpochShort(it) } ?: "To") + } + } + + if (showFromPicker) { + val pickerState = rememberDatePickerState( + initialSelectedDateMillis = currentFilter.minCreatedAt?.let { it * 1000 } + ) + DatePickerDialog( + onDismissRequest = { showFromPicker = false }, + confirmButton = { + TextButton(onClick = { + val epochSec = pickerState.selectedDateMillis?.div(1000) + onDateFilterSet(epochSec, currentFilter.maxCreatedAt) + showFromPicker = false + }) { Text("OK") } + }, + dismissButton = { + TextButton(onClick = { showFromPicker = false }) { Text("Cancel") } + } + ) { DatePicker(state = pickerState) } + } + + if (showToPicker) { + val pickerState = rememberDatePickerState( + initialSelectedDateMillis = currentFilter.maxCreatedAt?.let { it * 1000 } + ) + DatePickerDialog( + onDismissRequest = { showToPicker = false }, + confirmButton = { + TextButton(onClick = { + // +86399 to make "To" date inclusive (end of day) + val epochSec = pickerState.selectedDateMillis?.div(1000)?.let { it + 86399L } + onDateFilterSet(currentFilter.minCreatedAt, epochSec) + showToPicker = false + }) { Text("OK") } + }, + dismissButton = { + TextButton(onClick = { showToPicker = false }) { Text("Cancel") } + } + ) { DatePicker(state = pickerState) } + } } } } @@ -901,8 +1027,11 @@ private fun StatusChip(status: String) { // ── Helpers ────────────────────────────────────────────────────────────────── +private fun formatEpochShort(epoch: Long): String = + java.time.format.DateTimeFormatter.ofPattern("dd MMM") + .withZone(java.time.ZoneId.systemDefault()) + .format(java.time.Instant.ofEpochSecond(epoch)) private fun formatTimestamp(createdAt: Long?, rawTime: String): String { - Log.d("TimestampDebug", "createdAt=$createdAt rawTime=$rawTime") val formatter = java.time.format.DateTimeFormatter .ofPattern("dd MMM yyyy, HH:mm") .withZone(java.time.ZoneId.systemDefault()) 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 31e5f09..90fed98 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryViewModel.kt @@ -27,12 +27,16 @@ data class PaymentFilter( val statuses: Set = emptySet(), val types: Set = emptySet(), val minAmountSat : Long? = null, - val maxAmountSat : Long? = null + val maxAmountSat : Long? = null, + val minCreatedAt : Long? = null, // epoch seconds, inclusive + val maxCreatedAt : Long? = null, // epoch seconds, inclusive + val datePreset : DatePreset? = null // which preset chip is active ) enum class DirectionFilter { ALL, OUTGOING, INCOMING } enum class StatusFilter { COMPLETED, PENDING, FAILED } +enum class DatePreset { THIS_WEEK, THIS_MONTH, THIS_YEAR } // ── ViewModel ───────────────────────────────────────────────────────────────── @@ -74,6 +78,34 @@ class HistoryViewModel( fun clearStatusFilter() { _filter.update { it.copy(statuses = emptySet()) } } fun clearTypeFilter() { _filter.update { it.copy(types = emptySet()) } } + fun setDateFilter(min: Long?, max: Long?, preset: DatePreset? = null) { + _filter.update { it.copy(minCreatedAt = min, maxCreatedAt = max, datePreset = preset) } + } + fun applyDatePreset(preset: DatePreset) { + val now = java.time.ZonedDateTime.now() + val (min, max) = when (preset) { + DatePreset.THIS_WEEK -> { + val start = now.with(java.time.DayOfWeek.MONDAY) + .truncatedTo(java.time.temporal.ChronoUnit.DAYS) + start.toEpochSecond() to now.toEpochSecond() + } + DatePreset.THIS_MONTH -> { + val start = now.withDayOfMonth(1) + .truncatedTo(java.time.temporal.ChronoUnit.DAYS) + start.toEpochSecond() to now.toEpochSecond() + } + DatePreset.THIS_YEAR -> { + val start = now.withDayOfYear(1) + .truncatedTo(java.time.temporal.ChronoUnit.DAYS) + start.toEpochSecond() to now.toEpochSecond() + } + } + setDateFilter(min, max, preset) + } + fun clearDateFilter() { + _filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) } + } + /** Distinct non-null tags present in the currently loaded list. */ val availableTypes: StateFlow> = _state .map { s -> @@ -118,6 +150,16 @@ class HistoryViewModel( (f.maxAmountSat == null || amt <= f.maxAmountSat) } } + .let { list -> + if (f.minCreatedAt == null && f.maxCreatedAt == null) list + else list.filter { payment -> + val epoch = payment.createdAt + ?: payment.time.toLongOrNull() // try the raw string field + ?: return@filter false // no usable timestamp → exclude + (f.minCreatedAt == null || epoch >= f.minCreatedAt) && + (f.maxCreatedAt == null || epoch <= f.maxCreatedAt) + } + } s.copy(payments = filtered) }.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading)