Adds pagination to transaction history

Implements pagination for the transaction history, enabling users
to navigate through their transactions in manageable chunks. This
improves performance and user experience, especially for users
with a large number of transactions. It also introduces total entry counts.
This commit is contained in:
padreug 2025-11-08 23:51:12 +01:00
parent 4b327a0aab
commit 69b8f6e2d3
4 changed files with 120 additions and 12 deletions

48
crud.py
View file

@ -266,14 +266,14 @@ async def get_entry_lines(journal_entry_id: str) -> list[EntryLine]:
return lines return lines
async def get_all_journal_entries(limit: int = 100) -> list[JournalEntry]: async def get_all_journal_entries(limit: int = 100, offset: int = 0) -> list[JournalEntry]:
entries_data = await db.fetchall( entries_data = await db.fetchall(
""" """
SELECT * FROM journal_entries SELECT * FROM journal_entries
ORDER BY entry_date DESC, created_at DESC ORDER BY entry_date DESC, created_at DESC
LIMIT :limit LIMIT :limit OFFSET :offset
""", """,
{"limit": limit}, {"limit": limit, "offset": offset},
) )
entries = [] entries = []
@ -301,7 +301,7 @@ async def get_all_journal_entries(limit: int = 100) -> list[JournalEntry]:
async def get_journal_entries_by_user( async def get_journal_entries_by_user(
user_id: str, limit: int = 100 user_id: str, limit: int = 100, offset: int = 0
) -> list[JournalEntry]: ) -> list[JournalEntry]:
"""Get journal entries that affect the user's accounts""" """Get journal entries that affect the user's accounts"""
# Get all user-specific accounts # Get all user-specific accounts
@ -320,6 +320,7 @@ async def get_journal_entries_by_user(
placeholders = ','.join([f":account_{i}" for i in range(len(account_ids))]) placeholders = ','.join([f":account_{i}" for i in range(len(account_ids))])
params = {f"account_{i}": acc_id for i, acc_id in enumerate(account_ids)} params = {f"account_{i}": acc_id for i, acc_id in enumerate(account_ids)}
params["limit"] = limit params["limit"] = limit
params["offset"] = offset
entries_data = await db.fetchall( entries_data = await db.fetchall(
f""" f"""
@ -328,7 +329,7 @@ async def get_journal_entries_by_user(
JOIN entry_lines el ON je.id = el.journal_entry_id JOIN entry_lines el ON je.id = el.journal_entry_id
WHERE el.account_id IN ({placeholders}) WHERE el.account_id IN ({placeholders})
ORDER BY je.entry_date DESC, je.created_at DESC ORDER BY je.entry_date DESC, je.created_at DESC
LIMIT :limit LIMIT :limit OFFSET :offset
""", """,
params, params,
) )
@ -357,6 +358,43 @@ async def get_journal_entries_by_user(
return entries return entries
async def count_all_journal_entries() -> int:
"""Count total number of journal entries"""
result = await db.fetchone(
"SELECT COUNT(*) as total FROM journal_entries"
)
return result["total"] if result else 0
async def count_journal_entries_by_user(user_id: str) -> int:
"""Count journal entries that affect the user's accounts"""
# Get all user-specific accounts
user_accounts = await db.fetchall(
"SELECT id FROM accounts WHERE user_id = :user_id",
{"user_id": user_id},
)
if not user_accounts:
return 0
account_ids = [acc["id"] for acc in user_accounts]
# Count journal entries that have lines affecting these accounts
placeholders = ','.join([f":account_{i}" for i in range(len(account_ids))])
params = {f"account_{i}": acc_id for i, acc_id in enumerate(account_ids)}
result = await db.fetchone(
f"""
SELECT COUNT(DISTINCT je.id) as total
FROM journal_entries je
JOIN entry_lines el ON je.id = el.journal_entry_id
WHERE el.account_id IN ({placeholders})
""",
params,
)
return result["total"] if result else 0
# ===== BALANCE AND REPORTING ===== # ===== BALANCE AND REPORTING =====

View file

@ -10,6 +10,13 @@ window.app = Vue.createApp({
balance: null, balance: null,
allUserBalances: [], allUserBalances: [],
transactions: [], transactions: [],
transactionPagination: {
total: 0,
limit: 20,
offset: 0,
has_next: false,
has_prev: false
},
accounts: [], accounts: [],
currencies: [], currencies: [],
users: [], users: [],
@ -306,18 +313,39 @@ window.app = Vue.createApp({
console.error('Error loading all user balances:', error) console.error('Error loading all user balances:', error)
} }
}, },
async loadTransactions() { async loadTransactions(offset = null) {
try { try {
// Use provided offset or current pagination offset
const currentOffset = offset !== null ? offset : this.transactionPagination.offset
const response = await LNbits.api.request( const response = await LNbits.api.request(
'GET', 'GET',
'/castle/api/v1/entries/user', `/castle/api/v1/entries/user?limit=${this.transactionPagination.limit}&offset=${currentOffset}`,
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
this.transactions = response.data
// Update transactions and pagination info
this.transactions = response.data.entries
this.transactionPagination.total = response.data.total
this.transactionPagination.offset = response.data.offset
this.transactionPagination.has_next = response.data.has_next
this.transactionPagination.has_prev = response.data.has_prev
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
}, },
nextTransactionsPage() {
if (this.transactionPagination.has_next) {
const newOffset = this.transactionPagination.offset + this.transactionPagination.limit
this.loadTransactions(newOffset)
}
},
prevTransactionsPage() {
if (this.transactionPagination.has_prev) {
const newOffset = Math.max(0, this.transactionPagination.offset - this.transactionPagination.limit)
this.loadTransactions(newOffset)
}
},
async loadAccounts() { async loadAccounts() {
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(

View file

@ -394,6 +394,35 @@
No transactions yet No transactions yet
</div> </div>
</q-card-section> </q-card-section>
<!-- Pagination Controls -->
<q-card-section v-if="transactionPagination.total > transactionPagination.limit" class="q-pt-none">
<div class="row items-center justify-between">
<div class="col-auto">
<q-btn
flat
dense
icon="chevron_left"
label="Previous"
:disable="!transactionPagination.has_prev"
@click="prevTransactionsPage"
/>
</div>
<div class="col text-center text-grey">
{% raw %}{{ transactionPagination.offset + 1 }} - {{ Math.min(transactionPagination.offset + transactionPagination.limit, transactionPagination.total) }} of {{ transactionPagination.total }}{% endraw %}
</div>
<div class="col-auto">
<q-btn
flat
dense
icon-right="chevron_right"
label="Next"
:disable="!transactionPagination.has_next"
@click="nextTransactionsPage"
/>
</div>
</div>
</q-card-section>
</q-card> </q-card>
<!-- Balance Assertions (Super User Only) --> <!-- Balance Assertions (Super User Only) -->

View file

@ -265,16 +265,29 @@ async def api_get_journal_entries(limit: int = 100) -> list[JournalEntry]:
@castle_api_router.get("/api/v1/entries/user") @castle_api_router.get("/api/v1/entries/user")
async def api_get_user_entries( async def api_get_user_entries(
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
limit: int = 100, limit: int = 20,
) -> list[JournalEntry]: offset: int = 0,
) -> dict:
"""Get journal entries that affect the current user's accounts""" """Get journal entries that affect the current user's accounts"""
from lnbits.settings import settings as lnbits_settings from lnbits.settings import settings as lnbits_settings
from .crud import count_all_journal_entries, count_journal_entries_by_user
# If super user, show all journal entries # If super user, show all journal entries
if wallet.wallet.user == lnbits_settings.super_user: if wallet.wallet.user == lnbits_settings.super_user:
return await get_all_journal_entries(limit) entries = await get_all_journal_entries(limit, offset)
total = await count_all_journal_entries()
else:
entries = await get_journal_entries_by_user(wallet.wallet.user, limit, offset)
total = await count_journal_entries_by_user(wallet.wallet.user)
return await get_journal_entries_by_user(wallet.wallet.user, limit) return {
"entries": entries,
"total": total,
"limit": limit,
"offset": offset,
"has_next": (offset + limit) < total,
"has_prev": offset > 0,
}
@castle_api_router.get("/api/v1/entries/pending") @castle_api_router.get("/api/v1/entries/pending")