refactor(libra): collapse type filter + voided switch into category chips
Each chip toggles inclusion of one bucket of rows. Every row belongs to
exactly one bucket — voided rows go to the Voided bucket regardless of
their underlying type — so the model is straightforward:
[Income] [Expenses] [Voided]
| | |
income expense voided
(non-voided only) (any type)
Defaults: Income + Expenses on, Voided off. Independent multi-select.
Empty selection shows the empty state with a 'select a category' hint
instead of an open-ended 'try a different time period'.
Replaces the previous 'type radio + voided switch' pair: same axes, one
control type, no left/right visual split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd71d10564
commit
b483674ebe
1 changed files with 40 additions and 43 deletions
|
|
@ -11,8 +11,6 @@ import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
|
|
@ -32,15 +30,26 @@ const isLoading = ref(false)
|
|||
const dateRangeType = ref<number | 'custom'>(15) // 15, 30, 60, or 'custom'
|
||||
const customStartDate = ref<string>('')
|
||||
const customEndDate = ref<string>('')
|
||||
const typeFilter = ref<'all' | 'income' | 'expense'>('all')
|
||||
const showVoided = ref(false)
|
||||
// Each chip is an inclusion toggle for one bucket of rows. Every row
|
||||
// belongs to exactly one bucket (voided rows go to 'voided' regardless
|
||||
// of their income/expense type). Default hides voided.
|
||||
type Category = 'income' | 'expense' | 'voided'
|
||||
|
||||
const typeFilterOptions = [
|
||||
{ label: 'All', value: 'all' as const },
|
||||
{ label: 'Income', value: 'income' as const },
|
||||
{ label: 'Expenses', value: 'expense' as const }
|
||||
const activeCategories = ref<Set<Category>>(new Set<Category>(['income', 'expense']))
|
||||
|
||||
const categoryChips: { label: string; value: Category }[] = [
|
||||
{ label: 'Income', value: 'income' },
|
||||
{ label: 'Expenses', value: 'expense' },
|
||||
{ label: 'Voided', value: 'voided' }
|
||||
]
|
||||
|
||||
function toggleCategory(cat: Category) {
|
||||
const next = new Set(activeCategories.value)
|
||||
if (next.has(cat)) next.delete(cat)
|
||||
else next.add(cat)
|
||||
activeCategories.value = next
|
||||
}
|
||||
|
||||
function isIncome(t: Transaction): boolean {
|
||||
return t.tags?.includes('income-entry') ?? false
|
||||
}
|
||||
|
|
@ -57,6 +66,14 @@ function isPending(t: Transaction): boolean {
|
|||
return t.flag === '!' && !isVoided(t)
|
||||
}
|
||||
|
||||
// Which chip bucket a row falls into. Voided always wins over type.
|
||||
function getBucket(t: Transaction): Category | null {
|
||||
if (isVoided(t)) return 'voided'
|
||||
if (isIncome(t)) return 'income'
|
||||
if (isExpense(t)) return 'expense'
|
||||
return null
|
||||
}
|
||||
|
||||
const walletKey = computed(() => user.value?.wallets?.[0]?.inkey)
|
||||
|
||||
// Fuzzy search state and configuration
|
||||
|
|
@ -82,23 +99,13 @@ const searchOptions: FuzzySearchOptions<Transaction> = {
|
|||
matchAllWhenSearchEmpty: true
|
||||
}
|
||||
|
||||
// Transactions to display: search results (or all), with voided hidden by
|
||||
// default and the type filter applied last.
|
||||
// Transactions to display: row passes if its bucket's chip is active.
|
||||
const transactionsToDisplay = computed(() => {
|
||||
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 === 'expense') return base.filter(isExpense)
|
||||
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
|
||||
return base.filter(t => {
|
||||
const bucket = getBucket(t)
|
||||
return bucket !== null && activeCategories.value.has(bucket)
|
||||
})
|
||||
})
|
||||
|
||||
// Handle search results
|
||||
|
|
@ -273,30 +280,21 @@ onMounted(() => {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Type Filter (All / Income / Expenses) + Show voided toggle -->
|
||||
<!-- Category chips: each chip toggles inclusion of one bucket
|
||||
of rows. Defaults: Income + Expenses on, Voided off. -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Filter class="h-4 w-4 text-muted-foreground" />
|
||||
<Button
|
||||
v-for="option in typeFilterOptions"
|
||||
:key="option.value"
|
||||
:variant="typeFilter === option.value ? 'default' : 'outline'"
|
||||
v-for="chip in categoryChips"
|
||||
:key="chip.value"
|
||||
:variant="activeCategories.has(chip.value) ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
class="h-7 md:h-8 px-3 text-xs"
|
||||
@click="typeFilter = option.value"
|
||||
@click="toggleCategory(chip.value)"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ option.label }}
|
||||
{{ chip.label }}
|
||||
</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>
|
||||
|
||||
<!-- Custom Date Range Inputs -->
|
||||
|
|
@ -352,9 +350,6 @@ onMounted(() => {
|
|||
<span v-else>
|
||||
{{ transactionsToDisplay.length }} transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
|
||||
</span>
|
||||
<span v-if="voidedHiddenCount > 0" class="ml-1">
|
||||
({{ voidedHiddenCount }} voided hidden)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
|
|
@ -369,7 +364,9 @@ onMounted(() => {
|
|||
<div v-else-if="transactionsToDisplay.length === 0" class="text-center py-12 px-4">
|
||||
<p class="text-muted-foreground">No transactions found</p>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
{{ searchResults.length > 0 ? 'Try a different search term' : 'Try selecting a different time period' }}
|
||||
<template v-if="searchResults.length > 0">Try a different search term</template>
|
||||
<template v-else-if="activeCategories.size === 0">Select a category above to see transactions</template>
|
||||
<template v-else>Try selecting a different time period or toggling more categories</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue