Files
lnbits/lnbits/static/js/components/lnbits-payment-list.js
T

541 lines
16 KiB
JavaScript

window.app.component('lnbits-payment-list', {
template: '#lnbits-payment-list',
props: ['wallet', 'paymentFilter'],
data() {
return {
payments: [],
paymentsTable: {
columns: [
{
name: 'time',
align: 'left',
label: this.$t('memo') + '/' + this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'amount',
align: 'right',
label: this.$t('amount'),
field: 'sat',
sortable: true
}
],
pagination: {
rowsPerPage: 10,
page: 1,
sortBy: 'time',
descending: true,
rowsNumber: 10
},
sortFields: [
{name: 'amount', label: 'Amount'},
{name: 'fee', label: 'Fee'},
{name: 'memo', label: 'Memo'},
{name: 'time', label: 'Creation Date'},
{name: 'updated_at', label: 'Last Updated'}
],
search: '',
loading: false
},
searchDate: {from: null, to: null},
searchStatus: {
success: true,
pending: true,
failed: false,
incoming: true,
outgoing: true
},
exportTagName: '',
exportPaymentTagList: [],
paymentsCSV: {
columns: [
{
name: 'status',
align: 'right',
label: this.$t('status'),
field: 'status'
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'time',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'amount',
align: 'right',
label: this.$t('amount'),
field: 'sat',
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee'),
field: 'fee'
},
{
name: 'tag',
align: 'right',
label: this.$t('tag'),
field: 'tag'
},
{
name: 'payment_hash',
align: 'right',
label: this.$t('payment_hash'),
field: 'payment_hash'
},
{
name: 'payment_proof',
align: 'right',
label: this.$t('payment_proof'),
field: 'payment_proof'
},
{
name: 'webhook',
align: 'right',
label: this.$t('webhook'),
field: 'webhook'
},
{
name: 'fiat_currency',
align: 'right',
label: 'Fiat Currency',
field: row => row.extra.wallet_fiat_currency
},
{
name: 'fiat_amount',
align: 'right',
label: 'Fiat Amount',
field: row => row.extra.wallet_fiat_amount
}
],
preimage: null,
loading: false
},
hodlInvoice: {
show: false,
payment: null,
preimage: null
},
selectedPayment: null,
filterLabels: []
}
},
computed: {
filteredPayments() {
const q = this.paymentsTable.search
if (!q || q === '') return this.payments
return LNbits.utils.search(this.payments, q)
},
paymentsOmitter() {
if (this.$q.screen.lt.md && this.g.mobileSimple) {
return this.payments.length > 0 ? [this.payments[0]] : []
}
return this.payments
},
pendingPaymentsExist() {
return this.payments.findIndex(payment => payment.pending) !== -1
}
},
methods: {
mapPayment(data) {
const obj = {
checking_id: data.checking_id,
status: data.status,
amount: data.amount,
fee: data.fee,
memo: data.memo,
time: data.time,
bolt11: data.bolt11,
preimage: data.preimage,
payment_hash: data.payment_hash,
expiry: data.expiry,
extra: data.extra ?? {},
wallet_id: data.wallet_id,
webhook: data.webhook,
webhook_status: data.webhook_status,
fiat_amount: data.fiat_amount,
fiat_currency: data.fiat_currency,
labels: data.labels
}
obj.date = this.utils.formatDate(data.created_at)
obj.dateFrom = this.utils.formatDateFrom(data.created_at)
obj.expirydate = this.utils.formatDate(data.expiry)
obj.expirydateFrom = this.utils.formatDateFrom(data.expiry)
obj.msat = obj.amount
obj.sat = obj.msat / 1000
obj.tag = obj.extra?.tag
obj.fsat = this.utils.formatSat(obj.sat)
obj.isIn = obj.amount > 0
obj.isOut = obj.amount < 0
obj.isPending = obj.status === 'pending'
obj.isPaid = obj.status === 'success'
obj.isFailed = obj.status === 'failed'
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
try {
obj.details = JSON.parse(data.extra?.details || '{}')
} catch {
obj.details = {extraDetails: data.extra?.details}
}
return obj
},
searchByDate() {
if (typeof this.searchDate === 'string') {
this.searchDate = {
from: this.searchDate,
to: this.searchDate
}
}
if (this.searchDate.from) {
this.paymentFilter['time[ge]'] = this.searchDate.from + 'T00:00:00'
}
if (this.searchDate.to) {
this.paymentFilter['time[le]'] = this.searchDate.to + 'T23:59:59'
}
this.fetchPayments()
},
searchByLabels(labels) {
if (!labels || labels.length === 0) {
this.clearLabelSeach()
return
}
this.filterLabels = labels
this.paymentsTable.filter['labels[every]'] = labels
this.fetchPayments()
},
clearDateSeach() {
this.searchDate = {from: null, to: null}
delete this.paymentFilter['time[ge]']
delete this.paymentFilter['time[le]']
this.fetchPayments()
},
clearLabelSeach() {
this.filterLabels = []
delete this.paymentsTable.filter['labels[every]']
this.fetchPayments()
},
fetchPayments(props) {
this.paymentsTable.loading = true
const params = LNbits.utils.prepareFilterQuery(
this.paymentsTable,
props,
this.paymentFilter
)
return LNbits.api
.getPayments(this.wallet, params)
.then(response => {
this.paymentsTable.pagination.rowsNumber = response.data.total
this.payments = response.data.data.map(this.mapPayment)
this.paymentsTable.loading = false
this.recheckPendingPayments()
})
.catch(err => {
this.paymentsTable.loading = false
if (g.user.admin) {
this.fetchPaymentsAsAdmin(this.wallet.id, params)
} else {
LNbits.utils.notifyApiError(err)
}
})
},
sortByColumn(columnName) {
if (this.paymentsTable.pagination.sortBy === columnName) {
this.paymentsTable.pagination.descending =
!this.paymentsTable.pagination.descending
} else {
this.paymentsTable.pagination.sortBy = columnName
this.paymentsTable.pagination.descending = false
}
this.fetchPayments()
},
fetchPaymentsAsAdmin(walletId, params) {
params = (params || '') + '&wallet_id=' + walletId
return LNbits.api
.request('GET', '/api/v1/payments/all/paginated?' + params)
.then(response => {
this.paymentsTable.loading = false
this.paymentsTable.pagination.rowsNumber = response.data.total
this.payments = response.data.data.map(this.mapPayment)
})
.catch(err => {
this.paymentsTable.loading = false
LNbits.utils.notifyApiError(err)
})
},
checkPayment(payment_hash) {
LNbits.api
.getPayment(this.wallet, payment_hash)
.then(res => {
this.update = !this.update
if (res.data.status == 'success') {
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_successful')
})
}
if (res.data.status == 'pending') {
Quasar.Notify.create({
type: 'info',
message: this.$t('payment_pending')
})
}
})
.catch(LNbits.utils.notifyApiError)
},
recheckPendingPayments() {
const pendingPayments = this.payments.filter(p => p.status === 'pending')
if (pendingPayments.length === 0) return
const params = [
'recheck_pending=true',
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
].join('&')
LNbits.api
.getPayments(this.wallet, params)
.then(response => {
let updatedPayments = 0
response.data.data.forEach(updatedPayment => {
if (updatedPayment.status !== 'pending') {
const index = this.payments.findIndex(
p => p.checking_id === updatedPayment.checking_id
)
if (index !== -1) {
this.payments.splice(index, 1, this.mapPayment(updatedPayment))
updatedPayments += 1
}
}
})
if (updatedPayments > 0) {
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_successful')
})
}
})
.catch(err => {
console.warn(err)
})
},
showHoldInvoiceDialog(payment) {
this.hodlInvoice.show = true
this.hodlInvoice.preimage = ''
this.hodlInvoice.payment = payment
},
cancelHoldInvoice(payment_hash) {
LNbits.api
.cancelInvoice(this.wallet, payment_hash)
.then(() => {
this.update = !this.update
Quasar.Notify.create({
type: 'positive',
message: this.$t('invoice_cancelled')
})
})
.catch(LNbits.utils.notifyApiError)
},
settleHoldInvoice(preimage) {
LNbits.api
.settleInvoice(this.wallet, preimage)
.then(() => {
this.update = !this.update
Quasar.Notify.create({
type: 'positive',
message: this.$t('invoice_settled')
})
})
.catch(LNbits.utils.notifyApiError)
},
paymentTableRowKey(row) {
return row.payment_hash + row.amount
},
async exportCSV(detailed = false) {
// status is important for export but it is not in paymentsTable
// because it is manually added with payment detail link and icons
// and would cause duplication in the list
const pagination = this.paymentsTable.pagination
const maxPages = 100
const limit = 1000
let payments = []
this.paymentsCSV.loading = true
try {
for (let page = 0; page < maxPages; page++) {
const query = {
sortby: pagination.sortBy ?? 'time',
direction: pagination.descending ? 'desc' : 'asc',
limit,
offset: page * limit
}
const params = new URLSearchParams(query)
const response = await LNbits.api.getPayments(this.wallet, params)
const pagePayments = response.data.data || []
payments = payments.concat(pagePayments.map(this.mapPayment))
if (
pagePayments.length < limit ||
payments.length >= response.data.total
) {
break
}
}
let columns = this.paymentsCSV.columns
if (detailed) {
if (this.exportPaymentTagList.length) {
payments = payments.filter(p =>
this.exportPaymentTagList.includes(p.tag)
)
}
const extraColumns = Object.keys(
payments.reduce((e, p) => ({...e, ...p.details}), {})
).map(col => ({
name: col,
align: 'right',
label:
col.charAt(0).toUpperCase() +
col.slice(1).replace(/([A-Z])/g, ' $1'),
field: row => row.details[col],
format: data =>
typeof data === 'object' ? JSON.stringify(data) : data
}))
columns = this.paymentsCSV.columns.concat(extraColumns)
}
LNbits.utils.exportCSV(
columns,
payments,
this.wallet.name + '-payments'
)
} catch (err) {
LNbits.utils.notifyApiError(err)
} finally {
this.paymentsCSV.loading = false
}
},
addFilterTag() {
if (!this.exportTagName) return
const value = this.exportTagName.trim()
this.exportPaymentTagList = this.exportPaymentTagList.filter(
v => v !== value
)
this.exportPaymentTagList.push(value)
this.exportTagName = ''
},
removeExportTag(value) {
this.exportPaymentTagList = this.exportPaymentTagList.filter(
v => v !== value
)
},
formatCurrency(amount, currency) {
try {
return LNbits.utils.formatCurrency(amount, currency)
} catch (e) {
console.error(e)
return `${amount} ???`
}
},
handleFilterChanged() {
const {success, pending, failed, incoming, outgoing} = this.searchStatus
let paymentFilter = this.paymentFilter || {}
delete paymentFilter['status[ne]']
delete paymentFilter['status[eq]']
if (success && pending && failed) {
// No status filter
} else if (success && pending) {
paymentFilter['status[ne]'] = 'failed'
} else if (success && failed) {
paymentFilter['status[ne]'] = 'pending'
} else if (failed && pending) {
paymentFilter['status[ne]'] = 'success'
} else if (success && !pending && !failed) {
paymentFilter['status[eq]'] = 'success'
} else if (pending && !success && !failed) {
paymentFilter['status[eq]'] = 'pending'
} else if (failed && !success && !pending) {
paymentFilter['status[eq]'] = 'failed'
}
delete paymentFilter['amount[ge]']
delete paymentFilter['amount[le]']
if ((incoming && outgoing) || (!incoming && !outgoing)) {
// do nothing
} else if (incoming && !outgoing) {
paymentFilter['amount[ge]'] = 0
} else if (outgoing && !incoming) {
paymentFilter['amount[le]'] = 0
}
this.paymentFilter = paymentFilter
},
async savePaymentLabels(labels) {
if (!this.selectedPayment) {
Quasar.Notify.create({
type: 'warning',
message: 'No payment selected'
})
return
}
try {
await LNbits.api.request(
'PUT',
`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,
this.wallet.adminkey,
{
labels: labels
}
)
const payment = this.payments.find(
p => p.checking_id === this.selectedPayment.checking_id
)
if (payment) {
payment.labels = [...labels]
}
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_labels_updated')
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
isLightColor(color) {
try {
return Quasar.colors.luminosity(color) > 0.5
} catch (e) {
console.warning(e)
return false
}
}
},
watch: {
'paymentsTable.search': {
handler() {
const props = {}
if (this.paymentsTable.search) {
props['search'] = this.paymentsTable.search
}
this.fetchPayments()
}
},
'g.updatePayments'() {
this.fetchPayments()
}
},
created() {
this.fetchPayments()
}
})