Adds super user balance overview
Implements functionality for super users to view a breakdown of outstanding balances for all users. This includes: - Adding an API endpoint to fetch all user balances. - Updating the frontend to display these balances in a table, accessible only to super users. - Modifying the balance calculation for the current user to reflect the total owed by or to the castle for super users. This provides super users with a comprehensive view of the castle's financial position.
This commit is contained in:
parent
cb7e4ee555
commit
b7e4e05469
4 changed files with 117 additions and 4 deletions
37
crud.py
37
crud.py
|
|
@ -317,6 +317,43 @@ async def get_user_balance(user_id: str) -> UserBalance:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_user_balances() -> list[UserBalance]:
|
||||||
|
"""Get balances for all users (used by castle to see who they owe)"""
|
||||||
|
# Get all user-specific accounts
|
||||||
|
all_accounts = await db.fetchall(
|
||||||
|
"SELECT * FROM accounts WHERE user_id IS NOT NULL",
|
||||||
|
{},
|
||||||
|
Account,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Group by user_id
|
||||||
|
users_dict = {}
|
||||||
|
for account in all_accounts:
|
||||||
|
if account.user_id not in users_dict:
|
||||||
|
users_dict[account.user_id] = []
|
||||||
|
users_dict[account.user_id].append(account)
|
||||||
|
|
||||||
|
# Calculate balance for each user
|
||||||
|
user_balances = []
|
||||||
|
for user_id, accounts in users_dict.items():
|
||||||
|
total_balance = 0
|
||||||
|
for account in accounts:
|
||||||
|
balance = await get_account_balance(account.id)
|
||||||
|
if account.account_type == AccountType.LIABILITY:
|
||||||
|
total_balance += balance
|
||||||
|
elif account.account_type == AccountType.ASSET:
|
||||||
|
total_balance -= balance
|
||||||
|
|
||||||
|
if total_balance != 0: # Only include users with non-zero balance
|
||||||
|
user_balances.append(
|
||||||
|
UserBalance(
|
||||||
|
user_id=user_id, balance=total_balance, accounts=accounts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_balances
|
||||||
|
|
||||||
|
|
||||||
async def get_account_transactions(
|
async def get_account_transactions(
|
||||||
account_id: str, limit: int = 100
|
account_id: str, limit: int = 100
|
||||||
) -> list[tuple[JournalEntry, EntryLine]]:
|
) -> list[tuple[JournalEntry, EntryLine]]:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ window.app = Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
balance: null,
|
balance: null,
|
||||||
|
allUserBalances: [],
|
||||||
transactions: [],
|
transactions: [],
|
||||||
accounts: [],
|
accounts: [],
|
||||||
currencies: [],
|
currencies: [],
|
||||||
|
|
@ -71,10 +72,27 @@ window.app = Vue.createApp({
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
this.balance = response.data
|
this.balance = response.data
|
||||||
|
|
||||||
|
// If super user, also load all user balances
|
||||||
|
if (this.isSuperUser) {
|
||||||
|
await this.loadAllUserBalances()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async loadAllUserBalances() {
|
||||||
|
try {
|
||||||
|
const response = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/castle/api/v1/balances/all',
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
this.allUserBalances = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading all user balances:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadTransactions() {
|
async loadTransactions() {
|
||||||
try {
|
try {
|
||||||
const response = await LNbits.api.request(
|
const response = await LNbits.api.request(
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="balance !== null">
|
<div v-if="balance !== null">
|
||||||
<div class="text-h4" :class="balance.balance >= 0 ? 'text-positive' : 'text-negative'">
|
<div class="text-h4" :class="balance.balance >= 0 ? 'text-negative' : 'text-positive'">
|
||||||
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
|
{% raw %}{{ formatSats(Math.abs(balance.balance)) }} sats{% endraw %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-subtitle2">
|
<div class="text-subtitle2" v-if="isSuperUser">
|
||||||
|
{% raw %}{{ balance.balance > 0 ? 'Total you owe' : balance.balance < 0 ? 'Total owed to you' : 'No outstanding balances' }}{% endraw %}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2" v-else>
|
||||||
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
|
{% raw %}{{ balance.balance >= 0 ? 'Castle owes you' : 'You owe Castle' }}{% endraw %}
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="balance.balance < 0"
|
v-if="balance.balance < 0 && !isSuperUser"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-mt-md"
|
class="q-mt-md"
|
||||||
@click="showPayBalanceDialog"
|
@click="showPayBalanceDialog"
|
||||||
|
|
@ -98,6 +101,40 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
<!-- User Balances Breakdown (Super User Only) -->
|
||||||
|
<q-card v-if="isSuperUser && allUserBalances.length > 0">
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="q-my-none q-mb-md">Outstanding Balances by User</h6>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
:rows="allUserBalances"
|
||||||
|
:columns="[
|
||||||
|
{name: 'user', label: 'User ID', field: 'user_id', align: 'left'},
|
||||||
|
{name: 'balance', label: 'Amount Owed', field: 'balance', align: 'right'}
|
||||||
|
]"
|
||||||
|
row-key="user_id"
|
||||||
|
hide-pagination
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
>
|
||||||
|
<template v-slot:body-cell-user="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<div class="text-caption">{% raw %}{{ props.row.user_id.substring(0, 16) }}...{% endraw %}</div>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body-cell-balance="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<div :class="props.row.balance > 0 ? 'text-negative' : 'text-positive'">
|
||||||
|
{% raw %}{{ formatSats(Math.abs(props.row.balance)) }} sats{% endraw %}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
{% raw %}{{ props.row.balance > 0 ? 'You owe' : 'Owes you' }}{% endraw %}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
|
||||||
23
views_api.py
23
views_api.py
|
|
@ -19,6 +19,7 @@ from .crud import (
|
||||||
get_account_transactions,
|
get_account_transactions,
|
||||||
get_all_accounts,
|
get_all_accounts,
|
||||||
get_all_journal_entries,
|
get_all_journal_entries,
|
||||||
|
get_all_user_balances,
|
||||||
get_journal_entries_by_user,
|
get_journal_entries_by_user,
|
||||||
get_journal_entry,
|
get_journal_entry,
|
||||||
get_or_create_user_account,
|
get_or_create_user_account,
|
||||||
|
|
@ -382,7 +383,19 @@ async def api_get_my_balance(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> UserBalance:
|
) -> UserBalance:
|
||||||
"""Get current user's balance with the Castle"""
|
"""Get current user's balance with the Castle"""
|
||||||
return await get_user_balance(wallet.wallet.id)
|
from lnbits.settings import settings as lnbits_settings
|
||||||
|
|
||||||
|
# If super user, show total castle liabilities (what castle owes to all users)
|
||||||
|
if wallet.wallet.user == lnbits_settings.super_user:
|
||||||
|
all_balances = await get_all_user_balances()
|
||||||
|
total_owed = sum(b.balance for b in all_balances if b.balance > 0)
|
||||||
|
# Return as castle's "balance" - positive means castle owes money
|
||||||
|
return UserBalance(
|
||||||
|
user_id=wallet.wallet.user, balance=total_owed, accounts=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# For regular users, show their individual balance
|
||||||
|
return await get_user_balance(wallet.wallet.user)
|
||||||
|
|
||||||
|
|
||||||
@castle_api_router.get("/api/v1/balance/{user_id}")
|
@castle_api_router.get("/api/v1/balance/{user_id}")
|
||||||
|
|
@ -391,6 +404,14 @@ async def api_get_user_balance(user_id: str) -> UserBalance:
|
||||||
return await get_user_balance(user_id)
|
return await get_user_balance(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@castle_api_router.get("/api/v1/balances/all")
|
||||||
|
async def api_get_all_balances(
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> list[UserBalance]:
|
||||||
|
"""Get all user balances (admin/super user only)"""
|
||||||
|
return await get_all_user_balances()
|
||||||
|
|
||||||
|
|
||||||
# ===== PAYMENT ENDPOINTS =====
|
# ===== PAYMENT ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue