Adds custom date range filtering to transactions

Enables users to filter transactions by a custom date range, providing more flexibility in viewing transaction history.

Prioritizes custom date range over preset days for filtering.

Displays a warning if a user attempts to apply a custom date range without selecting both start and end dates.
This commit is contained in:
Padreug 2025-12-14 12:47:23 +01:00
parent f2df2f543b
commit 1d2eb05c36
4 changed files with 162 additions and 23 deletions

View file

@ -855,13 +855,25 @@ class FavaClient:
logger.error(f"Failed to fetch accounts via BQL: {e}") logger.error(f"Failed to fetch accounts via BQL: {e}")
raise raise
async def get_journal_entries(self, days: int = None) -> List[Dict[str, Any]]: async def get_journal_entries(
self,
days: int = None,
start_date: str = None,
end_date: str = None
) -> List[Dict[str, Any]]:
""" """
Get journal entries from Fava (with entry hashes), optionally filtered by date. Get journal entries from Fava (with entry hashes), optionally filtered by date.
Args: Args:
days: If provided, only return entries from the last N days. days: If provided, only return entries from the last N days.
If None, returns all entries (default behavior). If None, returns all entries (default behavior).
start_date: ISO format date string (YYYY-MM-DD). If provided with end_date,
filters entries between start_date and end_date (inclusive).
end_date: ISO format date string (YYYY-MM-DD). If provided with start_date,
filters entries between start_date and end_date (inclusive).
Note:
If both days and start_date/end_date are provided, start_date/end_date takes precedence.
Returns: Returns:
List of entries (transactions, opens, closes, etc.) with entry_hash field. List of entries (transactions, opens, closes, etc.) with entry_hash field.
@ -872,6 +884,9 @@ class FavaClient:
# Get only last 30 days # Get only last 30 days
recent = await fava.get_journal_entries(days=30) recent = await fava.get_journal_entries(days=30)
# Get entries in custom date range
custom = await fava.get_journal_entries(start_date="2024-01-01", end_date="2024-01-31")
""" """
try: try:
async with httpx.AsyncClient(timeout=self.timeout) as client: async with httpx.AsyncClient(timeout=self.timeout) as client:
@ -881,9 +896,33 @@ class FavaClient:
entries = result.get("data", []) entries = result.get("data", [])
logger.info(f"Fava /journal returned {len(entries)} entries") logger.info(f"Fava /journal returned {len(entries)} entries")
# Filter by date if requested # Filter by date range or days
if days is not None: from datetime import datetime, timedelta
from datetime import datetime, timedelta
# Use date range if both start_date and end_date are provided
if start_date and end_date:
try:
filter_start = datetime.strptime(start_date, "%Y-%m-%d").date()
filter_end = datetime.strptime(end_date, "%Y-%m-%d").date()
filtered_entries = []
for e in entries:
entry_date_str = e.get("date")
if entry_date_str:
try:
entry_date = datetime.strptime(entry_date_str, "%Y-%m-%d").date()
if filter_start <= entry_date <= filter_end:
filtered_entries.append(e)
except (ValueError, TypeError):
# Include entries with invalid dates (shouldn't happen)
filtered_entries.append(e)
logger.info(f"Filtered to {len(filtered_entries)} entries between {start_date} and {end_date}")
entries = filtered_entries
except ValueError as e:
logger.error(f"Invalid date format: {e}")
# Return all entries if date parsing fails
# Fall back to days filter if no date range provided
elif days is not None:
cutoff_date = (datetime.now() - timedelta(days=days)).date() cutoff_date = (datetime.now() - timedelta(days=days)).date()
filtered_entries = [] filtered_entries = []
for e in entries: for e in entries:

View file

@ -19,7 +19,9 @@ window.app = Vue.createApp({
transactionFilter: { transactionFilter: {
user_id: null, // For filtering by user user_id: null, // For filtering by user
account_type: null, // For filtering by receivable/payable (asset/liability) account_type: null, // For filtering by receivable/payable (asset/liability)
days: 5 // Number of days to fetch (5, 30, 60, 90) dateRangeType: 15, // Preset days (15, 30, 60) or 'custom'
startDate: null, // For custom date range (YYYY-MM-DD)
endDate: null // For custom date range (YYYY-MM-DD)
}, },
accounts: [], accounts: [],
currencies: [], currencies: [],
@ -362,9 +364,16 @@ window.app = Vue.createApp({
// Build query params with filters // Build query params with filters
let queryParams = `limit=${limit}&offset=${currentOffset}` let queryParams = `limit=${limit}&offset=${currentOffset}`
// Add days filter (default 5) // Add date filter - custom range takes precedence over preset days
const days = this.transactionFilter.days || 5 if (this.transactionFilter.dateRangeType === 'custom' && this.transactionFilter.startDate && this.transactionFilter.endDate) {
queryParams += `&days=${days}` // Dates are already in YYYY-MM-DD format from q-date with mask
queryParams += `&start_date=${this.transactionFilter.startDate}`
queryParams += `&end_date=${this.transactionFilter.endDate}`
} else {
// Use preset days filter
const days = typeof this.transactionFilter.dateRangeType === 'number' ? this.transactionFilter.dateRangeType : 15
queryParams += `&days=${days}`
}
if (this.transactionFilter.user_id) { if (this.transactionFilter.user_id) {
queryParams += `&filter_user_id=${this.transactionFilter.user_id}` queryParams += `&filter_user_id=${this.transactionFilter.user_id}`
@ -403,11 +412,30 @@ window.app = Vue.createApp({
this.transactionPagination.offset = 0 this.transactionPagination.offset = 0
this.loadTransactions(0) this.loadTransactions(0)
}, },
setTransactionDays(days) { onDateRangeTypeChange(value) {
// Update days filter and reload from first page // Handle date range type change (preset days or custom)
this.transactionFilter.days = days if (value !== 'custom') {
this.transactionPagination.offset = 0 // Clear custom date range when switching to preset days
this.loadTransactions(0) this.transactionFilter.startDate = null
this.transactionFilter.endDate = null
// Load transactions with preset days
this.transactionPagination.offset = 0
this.loadTransactions(0)
}
// If switching to custom, don't load until user provides dates
},
applyCustomDateRange() {
// Apply custom date range filter
if (this.transactionFilter.startDate && this.transactionFilter.endDate) {
this.transactionPagination.offset = 0
this.loadTransactions(0)
} else {
this.$q.notify({
type: 'warning',
message: 'Please select both start and end dates',
timeout: 3000
})
}
}, },
nextTransactionsPage() { nextTransactionsPage() {
if (this.transactionPagination.has_next) { if (this.transactionPagination.has_next) {

View file

@ -337,23 +337,84 @@
</div> </div>
<!-- Date Range Selector --> <!-- Date Range Selector -->
<div class="row q-mb-md"> <div class="row q-mb-md q-gutter-md">
<div class="col-auto"> <div class="col-auto">
<div class="text-caption text-grey q-mb-xs">Show transactions from:</div> <div class="text-caption text-grey q-mb-xs">Show transactions from:</div>
<q-btn-toggle <q-btn-toggle
v-model="transactionFilter.days" v-model="transactionFilter.dateRangeType"
toggle-color="primary" toggle-color="primary"
:options="[ :options="[
{label: 'Last 5 days', value: 5}, {label: 'Last 15 days', value: 15},
{label: 'Last 30 days', value: 30}, {label: 'Last 30 days', value: 30},
{label: 'Last 60 days', value: 60}, {label: 'Last 60 days', value: 60},
{label: 'Last 90 days', value: 90} {label: 'Custom Range', value: 'custom'}
]" ]"
@update:model-value="setTransactionDays" @update:model-value="onDateRangeTypeChange"
dense dense
unelevated unelevated
/> />
</div> </div>
<!-- Custom Date Range Inputs -->
<div v-if="transactionFilter.dateRangeType === 'custom'" class="col-auto row q-gutter-sm items-end">
<div class="col-auto">
<div class="text-caption text-grey q-mb-xs">From:</div>
<q-input
v-model="transactionFilter.startDate"
type="date"
outlined
dense
>
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date
v-model="transactionFilter.startDate"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="col-auto">
<div class="text-caption text-grey q-mb-xs">To:</div>
<q-input
v-model="transactionFilter.endDate"
type="date"
outlined
dense
>
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date
v-model="transactionFilter.endDate"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="col-auto">
<q-btn
color="primary"
label="Apply"
@click="applyCustomDateRange"
:disable="!transactionFilter.startDate || !transactionFilter.endDate"
unelevated
/>
</div>
</div>
</div> </div>
<!-- Filter Bar (Super User Only) --> <!-- Filter Bar (Super User Only) -->

View file

@ -399,7 +399,9 @@ async def api_get_user_entries(
offset: int = 0, offset: int = 0,
filter_user_id: str = None, filter_user_id: str = None,
filter_account_type: str = None, # 'asset' for receivable, 'liability' for payable filter_account_type: str = None, # 'asset' for receivable, 'liability' for payable
days: int = 5, # Default 5 days, options: 5, 30, 60, 90 days: int = 15, # Default 15 days, options: 15, 30, 60
start_date: str = None, # ISO format: YYYY-MM-DD
end_date: str = None, # ISO format: YYYY-MM-DD
) -> dict: ) -> dict:
""" """
Get journal entries that affect the current user's accounts from Fava/Beancount. Get journal entries that affect the current user's accounts from Fava/Beancount.
@ -407,7 +409,12 @@ async def api_get_user_entries(
Returns transactions in reverse chronological order with optional filtering. Returns transactions in reverse chronological order with optional filtering.
Args: Args:
days: Number of days to fetch (default: 5, options: 5, 30, 60, 90) days: Number of days to fetch (default: 15, options: 15, 30, 60)
start_date: Start date for custom range (YYYY-MM-DD). Requires end_date.
end_date: End date for custom range (YYYY-MM-DD). Requires start_date.
Note:
If both days and start_date/end_date are provided, start_date/end_date takes precedence.
""" """
from lnbits.settings import settings as lnbits_settings from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client from .fava_client import get_fava_client
@ -422,9 +429,13 @@ async def api_get_user_entries(
# Regular user can only see their own entries # Regular user can only see their own entries
target_user_id = wallet.wallet.user target_user_id = wallet.wallet.user
# Get journal entries from Fava (default last 5 days for performance) # Get journal entries from Fava
# User can request 30, 60, or 90 days via query parameter # Priority: custom date range > days > default (5 days)
all_entries = await fava.get_journal_entries(days=days) all_entries = await fava.get_journal_entries(
days=days,
start_date=start_date,
end_date=end_date
)
# Filter and transform entries # Filter and transform entries
filtered_entries = [] filtered_entries = []