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:
parent
fd269f97ea
commit
dd71d10564
1 changed files with 45 additions and 7 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue