diff --git a/crud.py b/crud.py index 2004264..57eea2c 100644 --- a/crud.py +++ b/crud.py @@ -395,6 +395,92 @@ async def count_journal_entries_by_user(user_id: str) -> int: return result["total"] if result else 0 +async def get_journal_entries_by_user_and_account_type( + user_id: str, account_type: str, limit: int = 100, offset: int = 0 +) -> list[JournalEntry]: + """Get journal entries that affect the user's accounts filtered by account type""" + # Get all user-specific accounts of the specified type + user_accounts = await db.fetchall( + "SELECT id FROM accounts WHERE user_id = :user_id AND account_type = :account_type", + {"user_id": user_id, "account_type": account_type}, + ) + + if not user_accounts: + return [] + + account_ids = [acc["id"] for acc in user_accounts] + + # Get all 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)} + params["limit"] = limit + params["offset"] = offset + + entries_data = await db.fetchall( + f""" + SELECT DISTINCT je.* + FROM journal_entries je + JOIN entry_lines el ON je.id = el.journal_entry_id + WHERE el.account_id IN ({placeholders}) + ORDER BY je.entry_date DESC, je.created_at DESC + LIMIT :limit OFFSET :offset + """, + params, + ) + + entries = [] + for entry_data in entries_data: + # Parse flag and meta from database + from .models import JournalEntryFlag + flag = JournalEntryFlag(entry_data.get("flag", "*")) + meta = json.loads(entry_data.get("meta", "{}")) if entry_data.get("meta") else {} + + entry = JournalEntry( + id=entry_data["id"], + description=entry_data["description"], + entry_date=entry_data["entry_date"], + created_by=entry_data["created_by"], + created_at=entry_data["created_at"], + reference=entry_data["reference"], + flag=flag, + meta=meta, + lines=[], + ) + entry.lines = await get_entry_lines(entry.id) + entries.append(entry) + + return entries + + +async def count_journal_entries_by_user_and_account_type(user_id: str, account_type: str) -> int: + """Count journal entries that affect the user's accounts filtered by account type""" + # Get all user-specific accounts of the specified type + user_accounts = await db.fetchall( + "SELECT id FROM accounts WHERE user_id = :user_id AND account_type = :account_type", + {"user_id": user_id, "account_type": account_type}, + ) + + 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 ===== diff --git a/static/js/index.js b/static/js/index.js index 41ff3e4..cf9e262 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -18,7 +18,8 @@ window.app = Vue.createApp({ has_prev: false }, 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) }, accounts: [], currencies: [], @@ -189,14 +190,21 @@ window.app = Vue.createApp({ transactionColumns() { return [ { name: 'flag', label: 'Status', field: 'flag', align: 'left', sortable: true }, + { name: 'username', label: 'User', field: 'username', align: 'left', sortable: true }, { name: 'date', label: 'Date', field: 'entry_date', align: 'left', sortable: true }, { name: 'description', label: 'Description', field: 'description', align: 'left', sortable: false }, - { name: 'username', label: 'User', field: 'username', align: 'left', sortable: true }, { name: 'amount', label: 'Amount (sats)', field: 'amount', align: 'right', sortable: false }, { name: 'fiat', label: 'Fiat Amount', field: 'fiat', align: 'right', sortable: false }, { name: 'reference', label: 'Reference', field: 'reference', align: 'left', sortable: false } ] }, + accountTypeOptions() { + return [ + { label: 'All Types', value: null }, + { label: 'Receivable (User owes Castle)', value: 'asset' }, + { label: 'Payable (Castle owes User)', value: 'liability' } + ] + }, expenseAccounts() { return this.accounts.filter(a => a.account_type === 'expense') }, @@ -344,11 +352,14 @@ window.app = Vue.createApp({ const limit = parseInt(this.transactionPagination.limit) || 20 - // Build query params with filter + // Build query params with filters let queryParams = `limit=${limit}&offset=${currentOffset}` if (this.transactionFilter.user_id) { queryParams += `&filter_user_id=${this.transactionFilter.user_id}` } + if (this.transactionFilter.account_type) { + queryParams += `&filter_account_type=${this.transactionFilter.account_type}` + } const response = await LNbits.api.request( 'GET', @@ -373,6 +384,7 @@ window.app = Vue.createApp({ }, clearTransactionFilter() { this.transactionFilter.user_id = null + this.transactionFilter.account_type = null this.transactionPagination.offset = 0 this.loadTransactions(0) }, diff --git a/templates/castle/index.html b/templates/castle/index.html index 2de6dfb..c9b4f87 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -357,12 +357,31 @@ -