diff --git a/src/modules/expenses/views/TransactionsPage.vue b/src/modules/expenses/views/TransactionsPage.vue index a2772e6..fbec2d9 100644 --- a/src/modules/expenses/views/TransactionsPage.vue +++ b/src/modules/expenses/views/TransactionsPage.vue @@ -12,6 +12,10 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { + CheckCircle2, + Clock, + Flag, + XCircle, RefreshCw, Calendar, Filter @@ -26,26 +30,14 @@ const isLoading = ref(false) const dateRangeType = ref(15) // 15, 30, 60, or 'custom' const customStartDate = ref('') const customEndDate = ref('') -// 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 typeFilter = ref<'all' | 'income' | 'expense'>('all') -const activeCategories = ref>(new Set(['income', 'expense'])) - -const categoryChips: { label: string; value: Category }[] = [ - { label: 'Income', value: 'income' }, - { label: 'Expenses', value: 'expense' }, - { label: 'Voided', value: 'voided' } +const typeFilterOptions = [ + { label: 'All', value: 'all' as const }, + { label: 'Income', value: 'income' as const }, + { label: 'Expenses', value: 'expense' as const } ] -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 } @@ -58,18 +50,6 @@ function isVoided(t: Transaction): boolean { return t.tags?.includes('voided') ?? false } -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 @@ -95,13 +75,12 @@ const searchOptions: FuzzySearchOptions = { matchAllWhenSearchEmpty: true } -// Transactions to display: row passes if its bucket's chip is active. +// Transactions to display (search results or all transactions), filtered by type const transactionsToDisplay = computed(() => { const base = searchResults.value.length > 0 ? searchResults.value : transactions.value - return base.filter(t => { - const bucket = getBucket(t) - return bucket !== null && activeCategories.value.has(bucket) - }) + if (typeFilter.value === 'income') return base.filter(isIncome) + if (typeFilter.value === 'expense') return base.filter(isExpense) + return base }) // Handle search results @@ -133,28 +112,23 @@ function formatAmount(amount: number): string { return new Intl.NumberFormat('en-US').format(amount) } -// Income gets a leading '+', expense a leading '-'. -function getAmountSign(t: Transaction): string { - if (isIncome(t)) return '+' - if (isExpense(t)) return '-' - return '' -} - -// Color tint for the amount text. Voided entries drop to muted regardless -// of type since the strike-through carries the "ignore this" signal. -function getAmountColorClass(t: Transaction): string { - if (isVoided(t)) return 'line-through text-muted-foreground' - if (isIncome(t)) return 'text-green-600 dark:text-green-400' - if (isExpense(t)) return 'text-red-600 dark:text-red-400' - return '' -} - -// Tags that drive other visual channels (border / sign / strike-through) — -// suppressed from the badge row so it only carries user-added tags. -const TYPE_TAGS = new Set(['income-entry', 'expense-entry', 'voided']) - -function getDisplayTags(t: Transaction): string[] { - return (t.tags ?? []).filter(tag => !TYPE_TAGS.has(tag)) +// Get status icon and color. Voided entries (per libra convention) keep +// flag='!' and carry a 'voided' tag — surface that as the icon regardless +// of the underlying flag. +function getStatusInfo(transaction: Transaction) { + if (isVoided(transaction)) { + return { icon: XCircle, color: 'text-muted-foreground', label: 'Voided' } + } + switch (transaction.flag) { + case '*': + return { icon: CheckCircle2, color: 'text-green-600', label: 'Cleared' } + case '!': + return { icon: Clock, color: 'text-orange-600', label: 'Pending' } + case '#': + return { icon: Flag, color: 'text-red-600', label: 'Flagged' } + default: + return null + } } // Load transactions @@ -257,20 +231,19 @@ onMounted(() => { - +
@@ -325,7 +298,7 @@ onMounted(() => { Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }} - {{ transactionsToDisplay.length }} transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }} + {{ transactions.length }} transaction{{ transactions.length === 1 ? '' : 's' }} @@ -341,9 +314,7 @@ onMounted(() => {

No transactions found

- - - + {{ searchResults.length > 0 ? 'Try a different search term' : 'Try selecting a different time period' }}

@@ -352,19 +323,34 @@ onMounted(() => {
-

- {{ transaction.description }} -

+
+ + +

+ {{ transaction.description }} +

+

{{ formatDate(transaction.date) }}

@@ -372,17 +358,22 @@ onMounted(() => {
-

- {{ getAmountSign(transaction) }}{{ formatAmount(transaction.amount) }} sats +

+ {{ formatAmount(transaction.amount) }} sats

- {{ getAmountSign(transaction) }}{{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }} + {{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}

@@ -399,44 +390,20 @@ onMounted(() => { Ref: {{ transaction.reference }}
- -
+ +
- Income - - - Expense - - - Voided - - - Pending approval - - - {{ tag }} + {{ tag === 'voided' ? 'Voided' : tag }}