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:
parent
f2df2f543b
commit
1d2eb05c36
4 changed files with 162 additions and 23 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
// 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}`
|
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') {
|
||||||
|
// 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.transactionPagination.offset = 0
|
||||||
this.loadTransactions(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) {
|
||||||
|
|
|
||||||
|
|
@ -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) -->
|
||||||
|
|
|
||||||
21
views_api.py
21
views_api.py
|
|
@ -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 = []
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue