Updates journal entry retrieval to filter entries based on the user's accounts rather than the user ID. This ensures that users only see journal entries that directly affect their accounts. Also displays fiat amount in journal entries if available in the metadata.
548 lines
16 KiB
JavaScript
548 lines
16 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
|
|
}
|
|
}
|
|
},
|
|
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
|
|
}
|
|
},
|
|
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() {
|
|
// TODO: Show manual payment request dialog
|
|
this.$q.notify({
|
|
type: 'info',
|
|
message: 'Manual payment feature coming soon!',
|
|
timeout: 3000
|
|
})
|
|
},
|
|
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
|
|
},
|
|
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
|
|
}
|
|
},
|
|
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()
|
|
}
|
|
}
|
|
})
|