feat(libra): hide voided by default + add Pending badge

Voided transactions are noise in the day-to-day view (the user already
saw and rejected them), so default to hiding them. A Switch in the filter
row toggles 'Show voided'. When voided are present but hidden, the
results-count line shows '(N voided hidden)' so the toggle has a
discoverable hook.

Pending entries gain a yellow Pending badge symmetric with the red
Voided badge — both signal 'needs attention' states in the badge row,
while cleared entries stay unmarked (the default, quiet state).

Status / type encoding (icon = status, signed/colored amount = type)
is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-06 22:51:43 +02:00
commit dd71d10564

View file

@ -11,6 +11,8 @@ import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { import {
CheckCircle2, CheckCircle2,
Clock, Clock,
@ -31,6 +33,7 @@ const dateRangeType = ref<number | 'custom'>(15) // 15, 30, 60, or 'custom'
const customStartDate = ref<string>('') const customStartDate = ref<string>('')
const customEndDate = ref<string>('') const customEndDate = ref<string>('')
const typeFilter = ref<'all' | 'income' | 'expense'>('all') const typeFilter = ref<'all' | 'income' | 'expense'>('all')
const showVoided = ref(false)
const typeFilterOptions = [ const typeFilterOptions = [
{ label: 'All', value: 'all' as const }, { label: 'All', value: 'all' as const },
@ -50,6 +53,10 @@ function isVoided(t: Transaction): boolean {
return t.tags?.includes('voided') ?? false return t.tags?.includes('voided') ?? false
} }
function isPending(t: Transaction): boolean {
return t.flag === '!' && !isVoided(t)
}
const walletKey = computed(() => user.value?.wallets?.[0]?.inkey) const walletKey = computed(() => user.value?.wallets?.[0]?.inkey)
// Fuzzy search state and configuration // Fuzzy search state and configuration
@ -75,14 +82,25 @@ const searchOptions: FuzzySearchOptions<Transaction> = {
matchAllWhenSearchEmpty: true matchAllWhenSearchEmpty: true
} }
// Transactions to display (search results or all transactions), filtered by type // Transactions to display: search results (or all), with voided hidden by
// default and the type filter applied last.
const transactionsToDisplay = computed(() => { const transactionsToDisplay = computed(() => {
const base = searchResults.value.length > 0 ? searchResults.value : transactions.value let base = searchResults.value.length > 0 ? searchResults.value : transactions.value
if (!showVoided.value) base = base.filter(t => !isVoided(t))
if (typeFilter.value === 'income') return base.filter(isIncome) if (typeFilter.value === 'income') return base.filter(isIncome)
if (typeFilter.value === 'expense') return base.filter(isExpense) if (typeFilter.value === 'expense') return base.filter(isExpense)
return base return base
}) })
// Count of voided entries that are being hidden right now surfaced in
// the results-count line so the user knows the toggle has something to
// reveal.
const voidedHiddenCount = computed(() => {
if (showVoided.value) return 0
const base = searchResults.value.length > 0 ? searchResults.value : transactions.value
return base.filter(isVoided).length
})
// Handle search results // Handle search results
function handleSearchResults(results: Transaction[]) { function handleSearchResults(results: Transaction[]) {
searchResults.value = results searchResults.value = results
@ -255,7 +273,7 @@ onMounted(() => {
</Button> </Button>
</div> </div>
<!-- Type Filter (All / Income / Expenses) --> <!-- Type Filter (All / Income / Expenses) + Show voided toggle -->
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<Filter class="h-4 w-4 text-muted-foreground" /> <Filter class="h-4 w-4 text-muted-foreground" />
<Button <Button
@ -269,6 +287,16 @@ onMounted(() => {
> >
{{ option.label }} {{ option.label }}
</Button> </Button>
<div class="flex items-center gap-2 ml-auto">
<Switch
id="show-voided"
v-model="showVoided"
:disabled="isLoading"
/>
<Label for="show-voided" class="text-xs text-muted-foreground cursor-pointer">
Show voided
</Label>
</div>
</div> </div>
<!-- Custom Date Range Inputs --> <!-- Custom Date Range Inputs -->
@ -322,7 +350,10 @@ onMounted(() => {
Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }} Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
</span> </span>
<span v-else> <span v-else>
{{ transactions.length }} transaction{{ transactions.length === 1 ? '' : 's' }} {{ transactionsToDisplay.length }} transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
</span>
<span v-if="voidedHiddenCount > 0" class="ml-1">
({{ voidedHiddenCount }} voided hidden)
</span> </span>
</div> </div>
@ -405,11 +436,11 @@ onMounted(() => {
<span class="font-medium">Ref:</span> {{ transaction.reference }} <span class="font-medium">Ref:</span> {{ transaction.reference }}
</div> </div>
<!-- Tags: Voided badge (status) + any user-added tags. Type <!-- Tags: status badge (Voided / Pending) + any user-added tags.
tags (income-entry / expense-entry) are intentionally Type tags (income-entry / expense-entry) are intentionally
suppressed they're encoded by the signed amount. --> suppressed they're encoded by the signed amount. -->
<div <div
v-if="isVoided(transaction) || getDisplayTags(transaction).length > 0" v-if="isVoided(transaction) || isPending(transaction) || getDisplayTags(transaction).length > 0"
class="flex flex-wrap gap-1 mt-2" class="flex flex-wrap gap-1 mt-2"
> >
<Badge <Badge
@ -419,6 +450,13 @@ onMounted(() => {
> >
Voided Voided
</Badge> </Badge>
<Badge
v-else-if="isPending(transaction)"
variant="secondary"
class="text-xs bg-yellow-100 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300"
>
Pending
</Badge>
<Badge <Badge
v-for="tag in getDisplayTags(transaction)" v-for="tag in getDisplayTags(transaction)"
:key="tag" :key="tag"