Add a payments page for admin (#2910)
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
window.PaymentsPageLogic = {
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
payments: [],
|
||||
dailyChartData: [],
|
||||
searchDate: {
|
||||
timeFrom: null,
|
||||
timeTo: null
|
||||
},
|
||||
searchData: {
|
||||
wallet_id: null,
|
||||
payment_hash: null,
|
||||
status: null,
|
||||
memo: null
|
||||
},
|
||||
chartData: {
|
||||
showPaymentStatus: true,
|
||||
showPaymentTags: true,
|
||||
showBalance: true,
|
||||
showWalletsSize: false,
|
||||
showBalanceInOut: false,
|
||||
showPaymentCountInOut: false
|
||||
},
|
||||
searchOptions: {
|
||||
status: []
|
||||
// tag: [] // not used, payments don't have tag, only the extra
|
||||
},
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'status',
|
||||
align: 'left',
|
||||
label: 'Status',
|
||||
field: 'status',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
align: 'left',
|
||||
label: 'Created At',
|
||||
field: 'created_at',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: 'Amount',
|
||||
field: 'amount',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'amountFiat',
|
||||
align: 'right',
|
||||
label: 'Fiat',
|
||||
field: 'amountFiat',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'left',
|
||||
label: 'Fee',
|
||||
field: 'fee',
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'tag',
|
||||
align: 'left',
|
||||
label: 'Tag',
|
||||
field: 'tag',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
align: 'left',
|
||||
label: 'Memo',
|
||||
field: 'memo',
|
||||
sortable: false,
|
||||
max_length: 20
|
||||
},
|
||||
{
|
||||
name: 'wallet_id',
|
||||
align: 'left',
|
||||
label: 'Wallet (ID)',
|
||||
field: 'wallet_id',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
name: 'payment_hash',
|
||||
align: 'left',
|
||||
label: 'Payment Hash',
|
||||
field: 'payment_hash',
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'created_at',
|
||||
rowsPerPage: 25,
|
||||
page: 1,
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: null,
|
||||
hideEmpty: true,
|
||||
loading: true
|
||||
},
|
||||
chartsReady: false,
|
||||
showDetails: false,
|
||||
paymentDetails: null,
|
||||
lnbitsBalance: 0
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.chartsReady = true
|
||||
await this.$nextTick()
|
||||
this.initCharts()
|
||||
this.searchDate.timeFrom = moment()
|
||||
.subtract(1, 'month')
|
||||
.format('YYYY-MM-DD')
|
||||
this.searchDate.timeTo = moment().format('YYYY-MM-DD')
|
||||
await this.fetchPayments()
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
async fetchPayments(props) {
|
||||
const filter = Object.entries(this.searchData).reduce(
|
||||
(a, [k, v]) => (v ? ((a[k] = v), a) : a),
|
||||
{}
|
||||
)
|
||||
|
||||
delete filter['time[ge]']
|
||||
delete filter['time[le]']
|
||||
if (this.searchDate.timeFrom) {
|
||||
filter['time[ge]'] = this.searchDate.timeFrom + 'T00:00:00'
|
||||
}
|
||||
if (this.searchDate.timeTo) {
|
||||
filter['time[le]'] = this.searchDate.timeTo + 'T23:59:59'
|
||||
}
|
||||
this.paymentsTable.filter = filter
|
||||
|
||||
try {
|
||||
const params = LNbits.utils.prepareFilterQuery(
|
||||
this.paymentsTable,
|
||||
props
|
||||
)
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/payments/all/paginated?${params}`
|
||||
)
|
||||
|
||||
this.paymentsTable.pagination.rowsNumber = data.total
|
||||
this.payments = data.data.map(p => {
|
||||
if (p.extra && p.extra.tag) {
|
||||
p.tag = p.extra.tag
|
||||
}
|
||||
p.timeFrom = moment(p.created_at).fromNow()
|
||||
|
||||
p.amount =
|
||||
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
||||
' sats'
|
||||
if (p.extra?.wallet_fiat_amount) {
|
||||
p.amountFiat = this.formatCurrency(
|
||||
p.extra.wallet_fiat_amount,
|
||||
p.extra.wallet_fiat_currency
|
||||
)
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.paymentsTable.loading = false
|
||||
this.updateCharts(props)
|
||||
}
|
||||
},
|
||||
async searchPaymentsBy(fieldName, fieldValue) {
|
||||
if (fieldName) {
|
||||
this.searchData[fieldName] = fieldValue
|
||||
}
|
||||
await this.fetchPayments()
|
||||
},
|
||||
|
||||
async removeCreatedFrom() {
|
||||
this.searchDate.timeFrom = null
|
||||
await this.fetchPayments()
|
||||
},
|
||||
async removeCreatedTo() {
|
||||
this.searchDate.timeTo = null
|
||||
await this.fetchPayments()
|
||||
},
|
||||
showDetailsToggle(payment) {
|
||||
this.paymentDetails = payment
|
||||
return (this.showDetails = !this.showDetails)
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return LNbits.utils.formatDateString(dateString)
|
||||
},
|
||||
formatCurrency(amount, currency) {
|
||||
try {
|
||||
return LNbits.utils.formatCurrency(amount, currency)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return `${amount} ???`
|
||||
}
|
||||
},
|
||||
shortify(value, max_length = 10) {
|
||||
valueLength = (value || '').length
|
||||
if (valueLength <= max_length) {
|
||||
return value
|
||||
}
|
||||
return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}`
|
||||
},
|
||||
async updateCharts(props) {
|
||||
let params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/payments/stats/count?${params}&count_by=status`
|
||||
)
|
||||
data.sort((a, b) => a.field - b.field)
|
||||
this.searchOptions.status = data
|
||||
.map(s => s.field)
|
||||
.sort()
|
||||
.reverse()
|
||||
this.paymentsStatusChart.data.datasets[0].data = data.map(s => s.total)
|
||||
this.paymentsStatusChart.data.labels = [...this.searchOptions.status]
|
||||
|
||||
this.paymentsStatusChart.update()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/payments/stats/wallets?${params}`
|
||||
)
|
||||
|
||||
const counts = data.map(w => w.balance / w.payments_count)
|
||||
|
||||
const min = Math.min(...counts)
|
||||
const max = Math.max(...counts)
|
||||
|
||||
const scale = val => {
|
||||
return Math.floor(3 + ((val - min) * (25 - 3)) / (max - min))
|
||||
}
|
||||
|
||||
const colors = this.randomColors(20)
|
||||
const walletsData = data.map((w, i) => {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
x: w.payments_count,
|
||||
y: w.balance,
|
||||
r: scale(Math.max(w.balance / w.payments_count, 5))
|
||||
}
|
||||
],
|
||||
label: w.wallet_name,
|
||||
wallet_id: w.wallet_id,
|
||||
backgroundColor: colors[i % 100],
|
||||
hoverOffset: 4
|
||||
}
|
||||
})
|
||||
this.paymentsWalletsChart.data.datasets = walletsData
|
||||
this.paymentsWalletsChart.update()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/payments/stats/count?${params}&count_by=tag`
|
||||
)
|
||||
this.searchOptions.tag = data.map(s => s.field)
|
||||
this.searchOptions.status.sort()
|
||||
this.paymentsTagsChart.data.datasets[0].data = data.map(rm => rm.total)
|
||||
this.paymentsTagsChart.data.labels = data.map(rm => rm.field || 'core')
|
||||
this.paymentsTagsChart.update()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const filter = Object.entries(this.searchData).reduce(
|
||||
(a, [k, v]) => (v ? ((a[k] = v), a) : a),
|
||||
{}
|
||||
)
|
||||
const paymentsTable = {...this.paymentsTable, filter: filter}
|
||||
const noTimeParams = LNbits.utils.prepareFilterQuery(
|
||||
paymentsTable,
|
||||
props
|
||||
)
|
||||
let {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/payments/stats/daily?${noTimeParams}`
|
||||
)
|
||||
|
||||
const timeFrom = this.searchDate.timeFrom + 'T00:00:00'
|
||||
const timeTo = this.searchDate.timeTo + 'T00:00:00'
|
||||
this.lnbitsBalance = data[data.length - 1].balance
|
||||
data = data.filter(p => {
|
||||
if (this.searchDate.timeFrom && this.searchDate.timeTo) {
|
||||
return p.date >= timeFrom && p.date <= timeTo
|
||||
}
|
||||
if (this.searchDate.timeFrom) {
|
||||
return p.date >= timeFrom
|
||||
}
|
||||
if (this.searchDate.timeTo) {
|
||||
return p.date <= timeTo
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
this.paymentsDailyChart.data.datasets = [
|
||||
{
|
||||
label: 'Balance',
|
||||
data: data.map(s => s.balance),
|
||||
pointStyle: false,
|
||||
borderWidth: 2,
|
||||
tension: 0.7,
|
||||
fill: 1
|
||||
},
|
||||
{
|
||||
label: 'Fees',
|
||||
data: data.map(s => s.fee),
|
||||
pointStyle: false,
|
||||
borderWidth: 1,
|
||||
tension: 0.4,
|
||||
fill: 1
|
||||
}
|
||||
]
|
||||
this.paymentsDailyChart.data.labels = data.map(s =>
|
||||
s.date.substring(0, 10)
|
||||
)
|
||||
this.paymentsDailyChart.update()
|
||||
|
||||
this.paymentsBalanceInOutChart.data.datasets = [
|
||||
{
|
||||
label: 'Incoming Payments Balance',
|
||||
data: data.map(s => s.balance_in)
|
||||
},
|
||||
{
|
||||
label: 'Outgoing Payments Balance',
|
||||
data: data.map(s => s.balance_out)
|
||||
}
|
||||
]
|
||||
this.paymentsBalanceInOutChart.data.labels = data.map(s =>
|
||||
s.date.substring(0, 10)
|
||||
)
|
||||
this.paymentsBalanceInOutChart.update()
|
||||
|
||||
this.paymentsCountInOutChart.data.datasets = [
|
||||
{
|
||||
label: 'Incoming Payments Count',
|
||||
data: data.map(s => s.count_in)
|
||||
},
|
||||
{
|
||||
label: 'Outgoing Payments Count',
|
||||
data: data.map(s => -s.count_out)
|
||||
}
|
||||
]
|
||||
this.paymentsCountInOutChart.data.labels = data.map(s =>
|
||||
s.date.substring(0, 10)
|
||||
)
|
||||
this.paymentsCountInOutChart.update()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async initCharts() {
|
||||
const chartData =
|
||||
this.$q.localStorage.getItem('lnbits.payments.chartData') || {}
|
||||
|
||||
this.chartData = {...this.chartData, ...chartData}
|
||||
if (!this.chartsReady) {
|
||||
console.warn('Charts are not ready yet. Initialization delayed.')
|
||||
return
|
||||
}
|
||||
this.paymentsStatusChart = new Chart(
|
||||
this.$refs.paymentsStatusChart.getContext('2d'),
|
||||
{
|
||||
type: 'doughnut',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchPaymentsBy('status', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgb(0, 205, 86)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)'
|
||||
],
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.paymentsWalletsChart = new Chart(
|
||||
this.$refs.paymentsWalletsChart.getContext('2d'),
|
||||
{
|
||||
type: 'bubble',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].datasetIndex
|
||||
this.searchPaymentsBy(
|
||||
'wallet_id',
|
||||
chart.data.datasets[i].wallet_id
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: this.randomColors(20),
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.paymentsTagsChart = new Chart(
|
||||
this.$refs.paymentsTagsChart.getContext('2d'),
|
||||
{
|
||||
type: 'pie',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Tags'
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchPaymentsBy('tag', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: this.randomColors(10),
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.paymentsDailyChart = new Chart(
|
||||
this.$refs.paymentsDailyChart.getContext('2d'),
|
||||
{
|
||||
type: 'line',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Tags'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: this.randomColors(10),
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.paymentsBalanceInOutChart = new Chart(
|
||||
this.$refs.paymentsBalanceInOutChart.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Tags'
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true
|
||||
},
|
||||
y: {
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: this.randomColors(50),
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.paymentsCountInOutChart = new Chart(
|
||||
this.$refs.paymentsCountInOutChart.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
title: {
|
||||
display: false,
|
||||
text: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true
|
||||
},
|
||||
y: {
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: this.randomColors(80),
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
saveChartsPreferences() {
|
||||
this.$q.localStorage.set('lnbits.payments.chartData', this.chartData)
|
||||
},
|
||||
randomColors(seed = 1) {
|
||||
const colors = []
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
for (let j = 1; j <= 10; j++) {
|
||||
colors.push(
|
||||
`rgb(${(j * seed * 33) % 200}, ${(71 * (i + j + seed)) % 255}, ${(i + seed * 30) % 255})`
|
||||
)
|
||||
}
|
||||
}
|
||||
return colors
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user