castle/static/js/index.js
padreug 1412359172 Adds settle receivable functionality
Implements a "Settle Receivable" feature for super users to record manual payments from users who owe money.

Introduces a dialog for inputting payment details (amount, method, description, reference), triggers an API call to record the transaction, and updates user balances and transaction history.

This is for non-lightning payments like cash, bank transfers, or checks.
2025-10-23 02:57:21 +02:00

998 lines
30 KiB
JavaScript

const mapJournalEntry = obj => {
return obj
}
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data() {
return {
balance: null,
allUserBalances: [],
transactions: [],
accounts: [],
currencies: [],
users: [],
settings: null,
userWalletSettings: null,
isAdmin: false,
isSuperUser: false,
castleWalletConfigured: false,
userWalletConfigured: false,
expenseDialog: {
show: false,
description: '',
amount: null,
expenseAccount: '',
isEquity: false,
reference: '',
currency: null,
loading: false
},
payDialog: {
show: false,
amount: null,
paymentRequest: null,
paymentHash: null,
checkWalletKey: null,
pollIntervalId: null,
loading: false
},
settingsDialog: {
show: false,
castleWalletId: '',
loading: false
},
userWalletDialog: {
show: false,
userWalletId: '',
loading: false
},
receivableDialog: {
show: false,
selectedUser: '',
description: '',
amount: null,
revenueAccount: '',
reference: '',
currency: null,
loading: false
},
manualPaymentDialog: {
show: false,
amount: null,
description: '',
loading: false
},
manualPaymentRequests: [],
pendingExpenses: [],
balanceAssertions: [],
assertionDialog: {
show: false,
account_id: '',
expected_balance_sats: 0,
expected_balance_fiat: null,
fiat_currency: null,
tolerance_sats: 0,
tolerance_fiat: 0,
loading: false
},
reconciliation: {
summary: null,
discrepancies: null,
checking: false,
showDiscrepancies: false
},
settleReceivableDialog: {
show: false,
user_id: '',
username: '',
maxAmount: 0,
amount: 0,
payment_method: 'cash',
description: '',
reference: '',
loading: false
}
}
},
watch: {
'payDialog.show': function(newVal) {
// When dialog is closed, stop polling
if (!newVal && this.payDialog.pollIntervalId) {
clearInterval(this.payDialog.pollIntervalId)
this.payDialog.pollIntervalId = null
}
}
},
computed: {
expenseAccounts() {
return this.accounts.filter(a => a.account_type === 'expense')
},
revenueAccounts() {
return this.accounts.filter(a => a.account_type === 'revenue')
},
amountLabel() {
if (this.expenseDialog.currency) {
return `Amount (${this.expenseDialog.currency}) *`
}
return 'Amount (sats) *'
},
receivableAmountLabel() {
if (this.receivableDialog.currency) {
return `Amount (${this.receivableDialog.currency}) *`
}
return 'Amount (sats) *'
},
currencyOptions() {
const options = [{label: 'Satoshis (default)', value: null}]
this.currencies.forEach(curr => {
options.push({label: curr, value: curr})
})
return options
},
userOptions() {
const options = []
this.users.forEach(user => {
options.push({
label: user.username,
value: user.user_id
})
})
return options
},
pendingManualPaymentRequests() {
return this.manualPaymentRequests.filter(r => r.status === 'pending')
},
failedAssertions() {
return this.balanceAssertions.filter(a => a.status === 'failed')
},
passedAssertions() {
return this.balanceAssertions.filter(a => a.status === 'passed')
},
allAccounts() {
return this.accounts
}
},
methods: {
async loadBalance() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/balance',
this.g.user.wallets[0].inkey
)
this.balance = response.data
// If super user, also load all user balances
if (this.isSuperUser) {
await this.loadAllUserBalances()
}
} catch (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() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/entries/user',
this.g.user.wallets[0].inkey
)
this.transactions = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadAccounts() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/accounts',
this.g.user.wallets[0].inkey
)
this.accounts = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadCurrencies() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/currencies',
this.g.user.wallets[0].inkey
)
this.currencies = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadUsers() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/users',
this.g.user.wallets[0].adminkey
)
this.users = response.data
} catch (error) {
console.error('Error loading users:', error)
}
},
async loadSettings() {
try {
// Try with admin key first to check settings
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/settings',
this.g.user.wallets[0].inkey
)
this.settings = response.data
this.castleWalletConfigured = !!(this.settings && this.settings.castle_wallet_id)
// Check if user is super user by seeing if they can access admin features
this.isSuperUser = this.g.user.super_user || false
this.isAdmin = this.g.user.admin || this.isSuperUser
} catch (error) {
// Settings not available
this.castleWalletConfigured = false
}
},
async loadUserWallet() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/user/wallet',
this.g.user.wallets[0].inkey
)
this.userWalletSettings = response.data
this.userWalletConfigured = !!(this.userWalletSettings && this.userWalletSettings.user_wallet_id)
} catch (error) {
this.userWalletConfigured = false
}
},
showSettingsDialog() {
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
this.settingsDialog.show = true
},
showUserWalletDialog() {
this.userWalletDialog.userWalletId = this.userWalletSettings?.user_wallet_id || ''
this.userWalletDialog.show = true
},
async submitSettings() {
if (!this.settingsDialog.castleWalletId) {
this.$q.notify({
type: 'warning',
message: 'Castle Wallet ID is required'
})
return
}
this.settingsDialog.loading = true
try {
await LNbits.api.request(
'PUT',
'/castle/api/v1/settings',
this.g.user.wallets[0].adminkey,
{
castle_wallet_id: this.settingsDialog.castleWalletId
}
)
this.$q.notify({
type: 'positive',
message: 'Settings updated successfully'
})
this.settingsDialog.show = false
await this.loadSettings()
// Reload user wallet to reflect castle wallet for super user
if (this.isSuperUser) {
await this.loadUserWallet()
}
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.settingsDialog.loading = false
}
},
async submitUserWallet() {
if (!this.userWalletDialog.userWalletId) {
this.$q.notify({
type: 'warning',
message: 'Wallet ID is required'
})
return
}
this.userWalletDialog.loading = true
try {
await LNbits.api.request(
'PUT',
'/castle/api/v1/user/wallet',
this.g.user.wallets[0].inkey,
{
user_wallet_id: this.userWalletDialog.userWalletId
}
)
this.$q.notify({
type: 'positive',
message: 'Wallet configured successfully'
})
this.userWalletDialog.show = false
await this.loadUserWallet()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.userWalletDialog.loading = false
}
},
async submitExpense() {
this.expenseDialog.loading = true
try {
await LNbits.api.request(
'POST',
'/castle/api/v1/entries/expense',
this.g.user.wallets[0].inkey,
{
description: this.expenseDialog.description,
amount: this.expenseDialog.amount,
expense_account: this.expenseDialog.expenseAccount,
is_equity: this.expenseDialog.isEquity,
user_wallet: this.g.user.wallets[0].id,
reference: this.expenseDialog.reference || null,
currency: this.expenseDialog.currency || null
}
)
this.$q.notify({
type: 'positive',
message: 'Expense added successfully'
})
this.expenseDialog.show = false
this.resetExpenseDialog()
await this.loadBalance()
await this.loadTransactions()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.expenseDialog.loading = false
}
},
async submitPayment() {
this.payDialog.loading = true
// Clear any existing polling interval
if (this.payDialog.pollIntervalId) {
clearInterval(this.payDialog.pollIntervalId)
this.payDialog.pollIntervalId = null
}
try {
// Generate an invoice on the Castle wallet
const response = await LNbits.api.request(
'POST',
'/castle/api/v1/generate-payment-invoice',
this.g.user.wallets[0].inkey,
{
amount: this.payDialog.amount
}
)
// Show the payment request in the dialog
this.payDialog.paymentRequest = response.data.payment_request
this.payDialog.paymentHash = response.data.payment_hash
this.payDialog.checkWalletKey = response.data.check_wallet_key
this.$q.notify({
type: 'positive',
message: 'Invoice generated! Scan QR code or copy to pay.',
timeout: 3000
})
// Poll for payment completion
this.pollForPayment(response.data.payment_hash, response.data.check_wallet_key)
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.payDialog.loading = false
}
},
async pollForPayment(paymentHash, checkWalletKey) {
// Clear any existing interval
if (this.payDialog.pollIntervalId) {
clearInterval(this.payDialog.pollIntervalId)
}
// Poll every 2 seconds for payment status
const checkPayment = async () => {
try {
const response = await LNbits.api.request(
'GET',
`/api/v1/payments/${paymentHash}`,
checkWalletKey
)
if (response.data && response.data.paid) {
// Record payment in accounting
try {
await LNbits.api.request(
'POST',
'/castle/api/v1/record-payment',
this.g.user.wallets[0].inkey,
{
payment_hash: paymentHash
}
)
} catch (error) {
console.error('Error recording payment:', error)
}
this.$q.notify({
type: 'positive',
message: 'Payment received! Your balance has been updated.',
timeout: 3000
})
this.payDialog.show = false
this.payDialog.paymentRequest = null
this.payDialog.amount = null
await this.loadBalance()
await this.loadTransactions()
return true
}
return false
} catch (error) {
// Silently ignore errors (payment might not exist yet)
return false
}
}
// Check every 2 seconds for up to 5 minutes
let attempts = 0
const maxAttempts = 150 // 5 minutes
this.payDialog.pollIntervalId = setInterval(async () => {
attempts++
const paid = await checkPayment()
if (paid || attempts >= maxAttempts) {
clearInterval(this.payDialog.pollIntervalId)
this.payDialog.pollIntervalId = null
}
}, 2000)
},
showManualPaymentOption() {
// This is for when user wants to pay their debt manually
// For now, just notify them to contact castle
this.$q.notify({
type: 'info',
message: 'Please contact Castle directly to arrange manual payment.',
timeout: 3000
})
},
showManualPaymentDialog() {
// This is for when Castle owes user and they want to request manual payment
this.manualPaymentDialog.amount = Math.abs(this.balance.balance)
this.manualPaymentDialog.description = ''
this.manualPaymentDialog.show = true
},
async submitManualPaymentRequest() {
this.manualPaymentDialog.loading = true
try {
await LNbits.api.request(
'POST',
'/castle/api/v1/manual-payment-request',
this.g.user.wallets[0].inkey,
{
amount: this.manualPaymentDialog.amount,
description: this.manualPaymentDialog.description
}
)
this.$q.notify({
type: 'positive',
message: 'Manual payment request submitted successfully!'
})
this.manualPaymentDialog.show = false
this.manualPaymentDialog.amount = null
this.manualPaymentDialog.description = ''
await this.loadManualPaymentRequests()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.manualPaymentDialog.loading = false
}
},
async loadManualPaymentRequests() {
try {
// If super user, load all requests; otherwise load user's own requests
const endpoint = this.isSuperUser
? '/castle/api/v1/manual-payment-requests/all'
: '/castle/api/v1/manual-payment-requests'
const key = this.isSuperUser
? this.g.user.wallets[0].adminkey
: this.g.user.wallets[0].inkey
const response = await LNbits.api.request(
'GET',
endpoint,
key,
this.isSuperUser ? {status: 'pending'} : {}
)
this.manualPaymentRequests = response.data
} catch (error) {
console.error('Error loading manual payment requests:', error)
}
},
async loadPendingExpenses() {
try {
if (!this.isSuperUser) return
if (!this.g.user.wallets[0]?.adminkey) return
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/entries/pending',
this.g.user.wallets[0].adminkey
)
this.pendingExpenses = response.data
} catch (error) {
console.error('Error loading pending expenses:', error)
}
},
async approveManualPaymentRequest(requestId) {
try {
await LNbits.api.request(
'POST',
`/castle/api/v1/manual-payment-requests/${requestId}/approve`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Manual payment request approved!'
})
await this.loadManualPaymentRequests()
await this.loadBalance()
await this.loadTransactions()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async rejectManualPaymentRequest(requestId) {
try {
await LNbits.api.request(
'POST',
`/castle/api/v1/manual-payment-requests/${requestId}/reject`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'warning',
message: 'Manual payment request rejected'
})
await this.loadManualPaymentRequests()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async approveExpense(entryId) {
try {
await LNbits.api.request(
'POST',
`/castle/api/v1/entries/${entryId}/approve`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Expense approved!'
})
await this.loadPendingExpenses()
await this.loadBalance()
await this.loadTransactions()
await this.loadAllUserBalances()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async rejectExpense(entryId) {
try {
await LNbits.api.request(
'POST',
`/castle/api/v1/entries/${entryId}/reject`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'warning',
message: 'Expense rejected'
})
await this.loadPendingExpenses()
await this.loadTransactions()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadBalanceAssertions() {
if (!this.isSuperUser) return
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/assertions',
this.g.user.wallets[0].adminkey
)
this.balanceAssertions = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async submitAssertion() {
this.assertionDialog.loading = true
try {
const payload = {
account_id: this.assertionDialog.account_id,
expected_balance_sats: this.assertionDialog.expected_balance_sats,
tolerance_sats: this.assertionDialog.tolerance_sats || 0
}
// Add fiat balance check if currency selected
if (this.assertionDialog.fiat_currency) {
payload.fiat_currency = this.assertionDialog.fiat_currency
payload.expected_balance_fiat = this.assertionDialog.expected_balance_fiat
payload.tolerance_fiat = this.assertionDialog.tolerance_fiat || 0
}
await LNbits.api.request(
'POST',
'/castle/api/v1/assertions',
this.g.user.wallets[0].adminkey,
payload
)
this.$q.notify({
type: 'positive',
message: 'Balance assertion passed!',
timeout: 3000
})
// Reset dialog
this.assertionDialog = {
show: false,
account_id: '',
expected_balance_sats: 0,
expected_balance_fiat: null,
fiat_currency: null,
tolerance_sats: 0,
tolerance_fiat: 0,
loading: false
}
// Reload assertions
await this.loadBalanceAssertions()
} catch (error) {
// Check if it's a 409 Conflict (assertion failed)
if (error.response && error.response.status === 409) {
const detail = error.response.data.detail
this.$q.notify({
type: 'negative',
message: `Assertion Failed! Expected: ${this.formatSats(detail.expected_sats)} sats, Got: ${this.formatSats(detail.actual_sats)} sats (diff: ${this.formatSats(detail.difference_sats)} sats)`,
timeout: 5000,
html: true
})
// Still reload to show the failed assertion
await this.loadBalanceAssertions()
this.assertionDialog.show = false
} else {
LNbits.utils.notifyApiError(error)
}
} finally {
this.assertionDialog.loading = false
}
},
async recheckAssertion(assertionId) {
try {
await LNbits.api.request(
'POST',
`/castle/api/v1/assertions/${assertionId}/check`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Assertion re-checked',
timeout: 2000
})
await this.loadBalanceAssertions()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async deleteAssertion(assertionId) {
try {
await LNbits.api.request(
'DELETE',
`/castle/api/v1/assertions/${assertionId}`,
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: 'positive',
message: 'Assertion deleted',
timeout: 2000
})
await this.loadBalanceAssertions()
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
getAccountName(accountId) {
const account = this.accounts.find(a => a.id === accountId)
return account ? account.name : accountId
},
async loadReconciliationSummary() {
if (!this.isSuperUser) return
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/reconciliation/summary',
this.g.user.wallets[0].adminkey
)
this.reconciliation.summary = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async loadReconciliationDiscrepancies() {
if (!this.isSuperUser) return
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/reconciliation/discrepancies',
this.g.user.wallets[0].adminkey
)
this.reconciliation.discrepancies = response.data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async runFullReconciliation() {
this.reconciliation.checking = true
try {
const response = await LNbits.api.request(
'POST',
'/castle/api/v1/reconciliation/check-all',
this.g.user.wallets[0].adminkey
)
const results = response.data
this.$q.notify({
type: results.failed > 0 ? 'warning' : 'positive',
message: `Checked ${results.checked} assertions: ${results.passed} passed, ${results.failed} failed`,
timeout: 3000
})
// Reload reconciliation data
await this.loadReconciliationSummary()
await this.loadReconciliationDiscrepancies()
await this.loadBalanceAssertions()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.reconciliation.checking = false
}
},
copyToClipboard(text) {
navigator.clipboard.writeText(text)
this.$q.notify({
type: 'positive',
message: 'Copied to clipboard!',
timeout: 1000
})
},
showPayBalanceDialog() {
// Clear any existing polling
if (this.payDialog.pollIntervalId) {
clearInterval(this.payDialog.pollIntervalId)
this.payDialog.pollIntervalId = null
}
this.payDialog.amount = Math.abs(this.balance.balance)
this.payDialog.paymentRequest = null
this.payDialog.paymentHash = null
this.payDialog.checkWalletKey = null
this.payDialog.show = true
},
async showReceivableDialog() {
// Load users if not already loaded
if (this.users.length === 0 && this.isSuperUser) {
await this.loadUsers()
}
this.receivableDialog.show = true
},
async submitReceivable() {
this.receivableDialog.loading = true
try {
await LNbits.api.request(
'POST',
'/castle/api/v1/entries/receivable',
this.g.user.wallets[0].adminkey,
{
description: this.receivableDialog.description,
amount: this.receivableDialog.amount,
revenue_account: this.receivableDialog.revenueAccount,
user_id: this.receivableDialog.selectedUser,
reference: this.receivableDialog.reference || null,
currency: this.receivableDialog.currency || null
}
)
this.$q.notify({
type: 'positive',
message: 'Receivable added successfully'
})
this.receivableDialog.show = false
this.resetReceivableDialog()
await this.loadBalance()
await this.loadTransactions()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.receivableDialog.loading = false
}
},
resetExpenseDialog() {
this.expenseDialog.description = ''
this.expenseDialog.amount = null
this.expenseDialog.expenseAccount = ''
this.expenseDialog.isEquity = false
this.expenseDialog.reference = ''
this.expenseDialog.currency = null
},
resetReceivableDialog() {
this.receivableDialog.selectedUser = ''
this.receivableDialog.description = ''
this.receivableDialog.amount = null
this.receivableDialog.revenueAccount = ''
this.receivableDialog.reference = ''
this.receivableDialog.currency = null
},
showSettleReceivableDialog(userBalance) {
// Only show for users who owe castle (negative balance)
if (userBalance.balance >= 0) return
this.settleReceivableDialog = {
show: true,
user_id: userBalance.user_id,
username: userBalance.username,
maxAmount: Math.abs(userBalance.balance), // Convert negative to positive
amount: Math.abs(userBalance.balance), // Default to full amount
payment_method: 'cash',
description: `Payment from ${userBalance.username}`,
reference: '',
loading: false
}
},
async submitSettleReceivable() {
this.settleReceivableDialog.loading = true
try {
const response = await LNbits.api.request(
'POST',
'/castle/api/v1/receivables/settle',
this.g.user.wallets[0].adminkey,
{
user_id: this.settleReceivableDialog.user_id,
amount: this.settleReceivableDialog.amount,
payment_method: this.settleReceivableDialog.payment_method,
description: this.settleReceivableDialog.description,
reference: this.settleReceivableDialog.reference || null
}
)
this.$q.notify({
type: 'positive',
message: response.data.message,
timeout: 3000
})
this.settleReceivableDialog.show = false
// Reload balances
await this.loadBalance()
await this.loadTransactions()
await this.loadAllUserBalances()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.settleReceivableDialog.loading = false
}
},
formatSats(amount) {
return new Intl.NumberFormat().format(amount)
},
formatFiat(amount, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount)
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString()
},
getTotalAmount(entry) {
if (!entry.lines || entry.lines.length === 0) return 0
return entry.lines.reduce((sum, line) => sum + line.debit + line.credit, 0) / 2
},
getEntryFiatAmount(entry) {
// Extract fiat amount from metadata if available
if (!entry.lines || entry.lines.length === 0) return null
for (const line of entry.lines) {
if (line.metadata && line.metadata.fiat_currency && line.metadata.fiat_amount) {
return this.formatFiat(line.metadata.fiat_amount, line.metadata.fiat_currency)
}
}
return null
},
isReceivable(entry) {
// Check if this is a receivable entry (user owes castle)
// Receivables have a debit to an "Accounts Receivable" account with the user's ID
if (!entry.lines || entry.lines.length === 0) return false
for (const line of entry.lines) {
// Look for a line with positive debit on an accounts receivable account
if (line.debit > 0) {
// Check if the account is associated with this user's receivables
const account = this.accounts.find(a => a.id === line.account_id)
if (account && account.name && account.name.includes('Assets:Receivable') && account.account_type === 'asset') {
return true
}
}
}
return false
},
isPayable(entry) {
// Check if this is a payable entry (castle owes user)
// Payables have a credit to an "Accounts Payable" account with the user's ID
if (!entry.lines || entry.lines.length === 0) return false
for (const line of entry.lines) {
// Look for a line with positive credit on an accounts payable account
if (line.credit > 0) {
// Check if the account is associated with this user's payables
const account = this.accounts.find(a => a.id === line.account_id)
if (account && account.name && account.name.includes('Liabilities:Payable') && account.account_type === 'liability') {
return true
}
}
}
return false
}
},
async created() {
// Load settings first to determine if user is super user
await this.loadSettings()
await this.loadUserWallet()
await this.loadBalance()
await this.loadTransactions()
await this.loadAccounts()
await this.loadCurrencies()
await this.loadManualPaymentRequests()
// Load users and pending expenses if super user
if (this.isSuperUser) {
await this.loadUsers()
await this.loadPendingExpenses()
await this.loadBalanceAssertions()
await this.loadReconciliationSummary()
await this.loadReconciliationDiscrepancies()
}
}
})