From 1d2eb05c3661f4fac630b129b642955c2c4844af Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 14 Dec 2025 12:47:23 +0100 Subject: [PATCH] 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. --- fava_client.py | 47 +++++++++++++++++++++--- static/js/index.js | 46 +++++++++++++++++++----- templates/castle/index.html | 71 ++++++++++++++++++++++++++++++++++--- views_api.py | 21 ++++++++--- 4 files changed, 162 insertions(+), 23 deletions(-) diff --git a/fava_client.py b/fava_client.py index fddce55..4cde16b 100644 --- a/fava_client.py +++ b/fava_client.py @@ -855,13 +855,25 @@ class FavaClient: logger.error(f"Failed to fetch accounts via BQL: {e}") 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. Args: days: If provided, only return entries from the last N days. 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: List of entries (transactions, opens, closes, etc.) with entry_hash field. @@ -872,6 +884,9 @@ class FavaClient: # Get only last 30 days 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: async with httpx.AsyncClient(timeout=self.timeout) as client: @@ -881,9 +896,33 @@ class FavaClient: entries = result.get("data", []) logger.info(f"Fava /journal returned {len(entries)} entries") - # Filter by date if requested - if days is not None: - from datetime import datetime, timedelta + # Filter by date range or days + 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() filtered_entries = [] for e in entries: diff --git a/static/js/index.js b/static/js/index.js index 5a1b767..318483b 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -19,7 +19,9 @@ window.app = Vue.createApp({ transactionFilter: { user_id: null, // For filtering by user 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: [], currencies: [], @@ -362,9 +364,16 @@ window.app = Vue.createApp({ // Build query params with filters let queryParams = `limit=${limit}&offset=${currentOffset}` - // Add days filter (default 5) - const days = this.transactionFilter.days || 5 - queryParams += `&days=${days}` + // Add date filter - custom range takes precedence over preset days + if (this.transactionFilter.dateRangeType === 'custom' && this.transactionFilter.startDate && this.transactionFilter.endDate) { + // 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) { queryParams += `&filter_user_id=${this.transactionFilter.user_id}` @@ -403,11 +412,30 @@ window.app = Vue.createApp({ this.transactionPagination.offset = 0 this.loadTransactions(0) }, - setTransactionDays(days) { - // Update days filter and reload from first page - this.transactionFilter.days = days - this.transactionPagination.offset = 0 - this.loadTransactions(0) + onDateRangeTypeChange(value) { + // Handle date range type change (preset days or custom) + if (value !== 'custom') { + // Clear custom date range when switching to preset days + 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() { if (this.transactionPagination.has_next) { diff --git a/templates/castle/index.html b/templates/castle/index.html index 97f5ee5..6648e6c 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -337,23 +337,84 @@ -
+
Show transactions from:
+ + +
+
+
From:
+ + + +
+
+
To:
+ + + +
+
+ +
+
diff --git a/views_api.py b/views_api.py index baaadc6..e94c9e6 100644 --- a/views_api.py +++ b/views_api.py @@ -399,7 +399,9 @@ async def api_get_user_entries( offset: int = 0, filter_user_id: str = None, 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: """ 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. 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 .fava_client import get_fava_client @@ -422,9 +429,13 @@ async def api_get_user_entries( # Regular user can only see their own entries target_user_id = wallet.wallet.user - # Get journal entries from Fava (default last 5 days for performance) - # User can request 30, 60, or 90 days via query parameter - all_entries = await fava.get_journal_entries(days=days) + # Get journal entries from Fava + # Priority: custom date range > days > default (5 days) + all_entries = await fava.get_journal_entries( + days=days, + start_date=start_date, + end_date=end_date + ) # Filter and transform entries filtered_entries = []