castle/static/js/index.js
padreug 2a14dd2e62 Adds receivable entry functionality
Implements the ability to record receivables (user owes the castle).

Adds API endpoint for creating receivable entries, which includes currency conversion to satoshis if fiat currency is provided.

Integrates a UI component (receivable dialog) for superusers to record debts owed by users, enhancing financial tracking capabilities.
2025-10-22 16:16:36 +02:00

425 lines
12 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,
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
}
}
},
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_wallet_id
})
})
return options
}
},
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
try {
// First, generate an invoice for the payment
const invoiceResponse = await LNbits.api.request(
'POST',
'/api/v1/payments',
this.g.user.wallets[0].inkey,
{
out: false,
amount: this.payDialog.amount,
memo: `Payment to Castle - ${this.payDialog.amount} sats`,
unit: 'sat'
}
)
// Show the invoice to the user
this.$q.notify({
type: 'positive',
message: 'Invoice generated! Pay it to settle your balance.',
timeout: 5000
})
// TODO: After payment, call /castle/api/v1/pay-balance to record it
// This would typically be done via a webhook or payment verification
this.payDialog.show = false
this.payDialog.amount = null
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.payDialog.loading = false
}
},
showPayBalanceDialog() {
this.payDialog.amount = Math.abs(this.balance.balance)
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_wallet: 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
},
formatSats(amount) {
return new Intl.NumberFormat().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
}
},
async created() {
await this.loadBalance()
await this.loadTransactions()
await this.loadAccounts()
await this.loadCurrencies()
await this.loadSettings()
await this.loadUserWallet()
// Load users if super user (for receivable dialog)
if (this.isSuperUser) {
await this.loadUsers()
}
}
})