mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-15 12:20:21 +01:00
feat: add payments table to user manager (#2491)
* feat: add payments table to user manager refactor payments table and payment chart into components and add them to usermanager * bundle
This commit is contained in:
parent
9933484558
commit
a5623ef7c3
11 changed files with 592 additions and 546 deletions
|
@ -102,226 +102,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
:style="$q.screen.lt.md ? {
|
||||
background: $q.screen.lt.md ? 'none !important': ''
|
||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||
, marginTop: $q.screen.lt.md ? '0px !important': ''
|
||||
} : ''"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col">
|
||||
<h5
|
||||
class="text-subtitle1 q-my-none"
|
||||
:v-text="$t('transactions')"
|
||||
></h5>
|
||||
</div>
|
||||
<div class="gt-sm col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
@click="exportCSV"
|
||||
:label="$t('export_csv')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="show_chart"
|
||||
color="grey"
|
||||
@click="showChart"
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('chart_tooltip')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
:style="$q.screen.lt.md ? {
|
||||
display: mobileSimple ? 'none !important': ''
|
||||
} : ''"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.search"
|
||||
debounce="300"
|
||||
:placeholder="$t('search_by_tag_memo_amount')"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="paymentsOmitter"
|
||||
:row-key="paymentTableRowKey"
|
||||
:columns="paymentsTable.columns"
|
||||
:pagination.sync="paymentsTable.pagination"
|
||||
:no-data-label="$t('no_transactions')"
|
||||
:filter="paymentsTable.search"
|
||||
:loading="paymentsTable.loading"
|
||||
:hide-header="mobileSimple"
|
||||
:hide-bottom="mobileSimple"
|
||||
@request="fetchPayments"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="props.row.isPaid"
|
||||
size="14px"
|
||||
:name="props.row.isOut ? 'call_made' : 'call_received'"
|
||||
:color="props.row.isOut ? 'pink' : 'green'"
|
||||
@click="props.expand = !props.expand"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
@click="props.expand = !props.expand"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('pending')"></span
|
||||
></q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
key="time"
|
||||
:props="props"
|
||||
style="white-space: normal; word-break: break-all"
|
||||
>
|
||||
<q-badge
|
||||
v-if="props.row.tag"
|
||||
color="yellow"
|
||||
text-color="black"
|
||||
>
|
||||
<a
|
||||
v-text="'#'+props.row.tag"
|
||||
class="inherit"
|
||||
:href="['/', props.row.tag].join('')"
|
||||
></a>
|
||||
</q-badge>
|
||||
<span v-text="props.row.memo"></span>
|
||||
<br />
|
||||
|
||||
<i>
|
||||
<span v-text="props.row.dateFrom"></span>
|
||||
<q-tooltip
|
||||
><span v-text="props.row.date"></span
|
||||
></q-tooltip>
|
||||
</i>
|
||||
</q-td>
|
||||
<q-td
|
||||
auto-width
|
||||
key="amount"
|
||||
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
|
||||
:props="props"
|
||||
v-text="parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100"
|
||||
>
|
||||
</q-td>
|
||||
<q-td auto-width key="amount" v-else :props="props">
|
||||
<span v-text="props.row.fsat"></span>
|
||||
<br />
|
||||
<i v-if="props.row.extra.wallet_fiat_currency">
|
||||
<span
|
||||
v-text="formatFiat(props.row.extra.wallet_fiat_currency, props.row.extra.wallet_fiat_amount)"
|
||||
></span>
|
||||
<br />
|
||||
</i>
|
||||
<i v-if="props.row.extra.fiat_currency">
|
||||
<span
|
||||
v-text="formatFiat(props.row.extra.fiat_currency, props.row.extra.fiat_amount)"
|
||||
></span>
|
||||
</i>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<div v-if="props.row.isIn && props.row.pending">
|
||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||
<span v-text="$t('invoice_waiting')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
<div
|
||||
v-if="props.row.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(props.row.bolt11)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isIn">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_received'"
|
||||
:color="'green'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_received')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_made'"
|
||||
:color="'pink'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_sent')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isOut && props.row.pending">
|
||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||
<span v-text="$t('outgoing_payment_pending')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<payment-list :wallet="this.g.wallet" :mobileSimple="mobileSimple" />
|
||||
</div>
|
||||
{% if HIDE_API %}
|
||||
<div class="col-12 col-md-4 q-gutter-y-md">
|
||||
|
@ -877,27 +658,6 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="paymentsChart.show" position="top">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<div class="row q-gutter-sm justify-between">
|
||||
<div class="text-h6">Payments Chart</div>
|
||||
<q-select
|
||||
label="Group"
|
||||
filled
|
||||
dense
|
||||
v-model="paymentsChart.group"
|
||||
style="min-width: 120px"
|
||||
:options="paymentsChart.groupOptions"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-tabs
|
||||
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
||||
active-class="px-0"
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<q-dialog v-model="walletDialog.show">
|
||||
<q-card class="q-pa-lg" style="width: 700px; max-width: 80vw">
|
||||
<h2 class="text-h6 q-mb-md">Wallets</h2>
|
||||
<q-dialog v-model="paymentDialog.show">
|
||||
<q-card class="q-pa-lg" style="width: 700px; max-width: 80vw">
|
||||
<payment-list :wallet="activeWallet" />
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-table :data="wallets" :columns="walletTable.columns">
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
|
@ -17,6 +22,15 @@
|
|||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
round
|
||||
icon="menu"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@click="showPayments(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Show Payments</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="!props.row.deleted"
|
||||
round
|
||||
|
|
|
@ -49,7 +49,7 @@ include "users/_createWalletDialog.html" %}
|
|||
<q-td>
|
||||
<q-btn
|
||||
round
|
||||
icon="menu"
|
||||
icon="list"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@click="fetchWallets(props.row.id)"
|
||||
|
|
|
@ -13,16 +13,28 @@ from lnbits.core.crud import (
|
|||
get_wallets,
|
||||
update_admin_settings,
|
||||
)
|
||||
from lnbits.core.models import Account, AccountFilters, CreateTopup, User, Wallet
|
||||
from lnbits.core.models import (
|
||||
Account,
|
||||
AccountFilters,
|
||||
CreateTopup,
|
||||
User,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits.core.services import update_wallet_balance
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
from lnbits.settings import EditableSettings, settings
|
||||
|
||||
users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_admin)])
|
||||
|
||||
|
||||
@users_router.get("/user")
|
||||
@users_router.get(
|
||||
"/user",
|
||||
name="get accounts",
|
||||
summary="Get paginated list of accounts",
|
||||
openapi_extra=generate_filter_params_openapi(AccountFilters),
|
||||
)
|
||||
async def api_get_users(
|
||||
filters: Filters = Depends(parse_filters(AccountFilters)),
|
||||
) -> Page[Account]:
|
||||
|
|
20
lnbits/static/bundle.min.js
vendored
20
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
156
lnbits/static/js/components/payment-chart.js
Normal file
156
lnbits/static/js/components/payment-chart.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
function generateChart(canvas, rawData) {
|
||||
const data = rawData.reduce(
|
||||
(previous, current) => {
|
||||
previous.labels.push(current.date)
|
||||
previous.income.push(current.income)
|
||||
previous.spending.push(current.spending)
|
||||
previous.cumulative.push(current.balance)
|
||||
return previous
|
||||
},
|
||||
{
|
||||
labels: [],
|
||||
income: [],
|
||||
spending: [],
|
||||
cumulative: []
|
||||
}
|
||||
)
|
||||
|
||||
return new Chart(canvas.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.cumulative,
|
||||
type: 'line',
|
||||
label: 'balance',
|
||||
backgroundColor: '#673ab7', // deep-purple
|
||||
borderColor: '#673ab7',
|
||||
borderWidth: 4,
|
||||
pointRadius: 3,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
data: data.income,
|
||||
type: 'bar',
|
||||
label: 'in',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||
},
|
||||
{
|
||||
data: data.spending,
|
||||
type: 'bar',
|
||||
label: 'out',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
text: 'Chart.js Combo Time Scale'
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: 'time',
|
||||
display: true,
|
||||
//offset: true,
|
||||
time: {
|
||||
minUnit: 'hour',
|
||||
stepSize: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// performance tweaks
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Vue.component('payment-chart', {
|
||||
name: 'payment-chart',
|
||||
props: ['wallet'],
|
||||
data: function () {
|
||||
return {
|
||||
paymentsChart: {
|
||||
show: false,
|
||||
group: {
|
||||
value: 'hour',
|
||||
label: 'Hour'
|
||||
},
|
||||
groupOptions: [
|
||||
{value: 'hour', label: 'Hour'},
|
||||
{value: 'day', label: 'Day'},
|
||||
{value: 'week', label: 'Week'},
|
||||
{value: 'month', label: 'Month'},
|
||||
{value: 'year', label: 'Year'}
|
||||
],
|
||||
instance: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showChart: function () {
|
||||
this.paymentsChart.show = true
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/api/v1/payments/history?group=' + this.paymentsChart.group.value,
|
||||
this.wallet.adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.$nextTick(() => {
|
||||
if (this.paymentsChart.instance) {
|
||||
this.paymentsChart.instance.destroy()
|
||||
}
|
||||
this.paymentsChart.instance = generateChart(
|
||||
this.$refs.canvas,
|
||||
response.data
|
||||
)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
this.paymentsChart.show = false
|
||||
})
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<span id="payment-chart">
|
||||
<q-btn dense flat round icon="show_chart" color="grey" @click="showChart" >
|
||||
<q-tooltip>
|
||||
<span v-text="$t('chart_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-dialog v-model="paymentsChart.show" position="top">
|
||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||
<q-card-section>
|
||||
<div class="row q-gutter-sm justify-between">
|
||||
<div class="text-h6">Payments Chart</div>
|
||||
<q-select label="Group" filled dense v-model="paymentsChart.group"
|
||||
style="min-width: 120px"
|
||||
:options="paymentsChart.groupOptions"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</span>
|
||||
`
|
||||
})
|
383
lnbits/static/js/components/payment-list.js
Normal file
383
lnbits/static/js/components/payment-list.js
Normal file
|
@ -0,0 +1,383 @@
|
|||
Vue.component('payment-list', {
|
||||
name: 'payment-list',
|
||||
props: ['wallet', 'mobileSimple', 'lazy'],
|
||||
data: function () {
|
||||
return {
|
||||
denomination: LNBITS_DENOMINATION,
|
||||
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') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
sortBy: 'time',
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: null,
|
||||
loading: false
|
||||
},
|
||||
paymentsCSV: {
|
||||
columns: [
|
||||
{
|
||||
name: 'pending',
|
||||
align: 'left',
|
||||
label: 'Pending',
|
||||
field: 'pending'
|
||||
},
|
||||
{
|
||||
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') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'right',
|
||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||
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
|
||||
}
|
||||
],
|
||||
filter: null,
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPayments: function () {
|
||||
var 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.mobileSimple) {
|
||||
return this.payments.length > 0 ? [this.payments[0]] : []
|
||||
}
|
||||
return this.payments
|
||||
},
|
||||
pendingPaymentsExist: function () {
|
||||
return this.payments.findIndex(payment => payment.pending) !== -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchPayments: function (props) {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||
return LNbits.api
|
||||
.getPayments(this.wallet, params)
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
paymentTableRowKey: function (row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
exportCSV: function () {
|
||||
// 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 query = {
|
||||
sortby: pagination.sortBy ?? 'time',
|
||||
direction: pagination.descending ? 'desc' : 'asc'
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.wallet, params).then(response => {
|
||||
const payments = response.data.data.map(LNbits.map.payment)
|
||||
LNbits.utils.exportCSV(
|
||||
this.paymentsCSV.columns,
|
||||
payments,
|
||||
this.wallet.name + '-payments'
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lazy: function (newVal) {
|
||||
debugger
|
||||
if (newVal === true) this.fetchPayments()
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.lazy === undefined) this.fetchPayments()
|
||||
},
|
||||
template: `
|
||||
<q-card
|
||||
:style="$q.screen.lt.md ? {
|
||||
background: $q.screen.lt.md ? 'none !important': ''
|
||||
, boxShadow: $q.screen.lt.md ? 'none !important': ''
|
||||
, marginTop: $q.screen.lt.md ? '0px !important': ''
|
||||
} : ''"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col">
|
||||
<h5
|
||||
class="text-subtitle1 q-my-none"
|
||||
:v-text="$t('transactions')"
|
||||
></h5>
|
||||
</div>
|
||||
<div class="gt-sm col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV" :label="$t('export_csv')" ></q-btn>
|
||||
<payment-chart :wallet="wallet" />
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
:style="$q.screen.lt.md ? {
|
||||
display: mobileSimple ? 'none !important': ''
|
||||
} : ''"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.search"
|
||||
debounce="300"
|
||||
:placeholder="$t('search_by_tag_memo_amount')"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="paymentsOmitter"
|
||||
:row-key="paymentTableRowKey"
|
||||
:columns="paymentsTable.columns"
|
||||
:pagination.sync="paymentsTable.pagination"
|
||||
:no-data-label="$t('no_transactions')"
|
||||
:filter="paymentsTable.search"
|
||||
:loading="paymentsTable.loading"
|
||||
:hide-header="mobileSimple"
|
||||
:hide-bottom="mobileSimple"
|
||||
@request="fetchPayments"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="props.row.isPaid"
|
||||
size="14px"
|
||||
:name="props.row.isOut ? 'call_made' : 'call_received'"
|
||||
:color="props.row.isOut ? 'pink' : 'green'"
|
||||
@click="props.expand = !props.expand"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
@click="props.expand = !props.expand"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('pending')"></span
|
||||
></q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
key="time"
|
||||
:props="props"
|
||||
style="white-space: normal; word-break: break-all"
|
||||
>
|
||||
<q-badge
|
||||
v-if="props.row.tag"
|
||||
color="yellow"
|
||||
text-color="black"
|
||||
>
|
||||
<a
|
||||
v-text="'#'+props.row.tag"
|
||||
class="inherit"
|
||||
:href="['/', props.row.tag].join('')"
|
||||
></a>
|
||||
</q-badge>
|
||||
<span v-text="props.row.memo"></span>
|
||||
<br />
|
||||
|
||||
<i>
|
||||
<span v-text="props.row.dateFrom"></span>
|
||||
<q-tooltip
|
||||
><span v-text="props.row.date"></span
|
||||
></q-tooltip>
|
||||
</i>
|
||||
</q-td>
|
||||
<q-td
|
||||
auto-width
|
||||
key="amount"
|
||||
v-if="denomination != 'sats'"
|
||||
:props="props"
|
||||
class="lol1"
|
||||
v-text="parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100"
|
||||
>
|
||||
</q-td>
|
||||
<q-td class="lol2" auto-width key="amount" v-else :props="props">
|
||||
<span v-text="props.row.fsat"></span>
|
||||
<br />
|
||||
<i v-if="props.row.extra.wallet_fiat_currency">
|
||||
<span
|
||||
v-text="LNbits.utils.formatCurrency(props.row.extra.wallet_fiat_currency, props.row.extra.wallet_fiat_amount)"
|
||||
></span>
|
||||
<br />
|
||||
</i>
|
||||
<i v-if="props.row.extra.fiat_currency">
|
||||
<span
|
||||
v-text="LNbits.utils.formatCurrency(props.row.extra.fiat_currency, props.row.extra.fiat_amount)"
|
||||
></span>
|
||||
</i>
|
||||
</q-td>
|
||||
|
||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<div v-if="props.row.isIn && props.row.pending">
|
||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||
<span v-text="$t('invoice_waiting')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
<div
|
||||
v-if="props.row.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<lnbits-qrcode
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(props.row.bolt11)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isIn">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_received'"
|
||||
:color="'green'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_received')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_made'"
|
||||
:color="'pink'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_sent')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isOut && props.row.pending">
|
||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||
<span v-text="$t('outgoing_payment_pending')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
`
|
||||
})
|
|
@ -4,10 +4,14 @@ new Vue({
|
|||
data: function () {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
activeWallet: {},
|
||||
wallet: {},
|
||||
cancel: {},
|
||||
users: [],
|
||||
wallets: [],
|
||||
paymentDialog: {
|
||||
show: false
|
||||
},
|
||||
walletDialog: {
|
||||
show: false
|
||||
},
|
||||
|
@ -324,6 +328,10 @@ new Vue({
|
|||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
showPayments(wallet_id) {
|
||||
this.activeWallet = this.wallets.find(wallet => wallet.id === wallet_id)
|
||||
this.paymentDialog.show = true
|
||||
},
|
||||
toggleAdmin(user_id) {
|
||||
LNbits.api
|
||||
.request('GET', `/users/api/v1/user/${user_id}/admin`)
|
||||
|
|
|
@ -1,90 +1,8 @@
|
|||
/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart, decryptLnurlPayAES */
|
||||
/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, decryptLnurlPayAES */
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
|
||||
function generateChart(canvas, rawData) {
|
||||
const data = rawData.reduce(
|
||||
(previous, current) => {
|
||||
previous.labels.push(current.date)
|
||||
previous.income.push(current.income)
|
||||
previous.spending.push(current.spending)
|
||||
previous.cumulative.push(current.balance)
|
||||
return previous
|
||||
},
|
||||
{
|
||||
labels: [],
|
||||
income: [],
|
||||
spending: [],
|
||||
cumulative: []
|
||||
}
|
||||
)
|
||||
|
||||
return new Chart(canvas.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.cumulative,
|
||||
type: 'line',
|
||||
label: 'balance',
|
||||
backgroundColor: '#673ab7', // deep-purple
|
||||
borderColor: '#673ab7',
|
||||
borderWidth: 4,
|
||||
pointRadius: 3,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
data: data.income,
|
||||
type: 'bar',
|
||||
label: 'in',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
|
||||
},
|
||||
{
|
||||
data: data.spending,
|
||||
type: 'bar',
|
||||
label: 'out',
|
||||
barPercentage: 0.75,
|
||||
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
text: 'Chart.js Combo Time Scale'
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: 'time',
|
||||
display: true,
|
||||
//offset: true,
|
||||
time: {
|
||||
minUnit: 'hour',
|
||||
stepSize: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// performance tweaks
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
|
@ -126,118 +44,6 @@ new Vue({
|
|||
camera: 'auto'
|
||||
}
|
||||
},
|
||||
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') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
sortBy: 'time',
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: null,
|
||||
loading: false
|
||||
},
|
||||
paymentsCSV: {
|
||||
columns: [
|
||||
{
|
||||
name: 'pending',
|
||||
align: 'left',
|
||||
label: 'Pending',
|
||||
field: 'pending'
|
||||
},
|
||||
{
|
||||
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') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'fee',
|
||||
align: 'right',
|
||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||
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
|
||||
}
|
||||
],
|
||||
filter: null,
|
||||
loading: false
|
||||
},
|
||||
paymentsChart: {
|
||||
show: false,
|
||||
group: {value: 'hour', label: 'Hour'},
|
||||
groupOptions: [
|
||||
{value: 'month', label: 'Month'},
|
||||
{value: 'day', label: 'Day'},
|
||||
{value: 'hour', label: 'Hour'}
|
||||
],
|
||||
instance: null
|
||||
},
|
||||
disclaimerDialog: {
|
||||
show: false,
|
||||
location: window.location
|
||||
|
@ -268,63 +74,21 @@ new Vue({
|
|||
)
|
||||
}
|
||||
},
|
||||
filteredPayments: function () {
|
||||
var 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.mobileSimple) {
|
||||
return this.payments.length > 0 ? [this.payments[0]] : []
|
||||
}
|
||||
return this.payments
|
||||
},
|
||||
canPay: function () {
|
||||
if (!this.parse.invoice) return false
|
||||
return this.parse.invoice.sat <= this.balance
|
||||
},
|
||||
pendingPaymentsExist: function () {
|
||||
return this.payments.findIndex(payment => payment.pending) !== -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
msatoshiFormat: function (value) {
|
||||
return LNbits.utils.formatSat(value / 1000)
|
||||
},
|
||||
paymentTableRowKey: function (row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
closeCamera: function () {
|
||||
this.parse.camera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.parse.camera.show = true
|
||||
},
|
||||
showChart: function () {
|
||||
this.paymentsChart.show = true
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/api/v1/payments/history?group=' + this.paymentsChart.group.value,
|
||||
this.g.wallet.adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.$nextTick(() => {
|
||||
if (this.paymentsChart.instance) {
|
||||
this.paymentsChart.instance.destroy()
|
||||
}
|
||||
this.paymentsChart.instance = generateChart(
|
||||
this.$refs.canvas,
|
||||
response.data
|
||||
)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
this.paymentsChart.show = false
|
||||
})
|
||||
},
|
||||
focusInput(el) {
|
||||
this.$nextTick(() => this.$refs[el].focus())
|
||||
},
|
||||
|
@ -359,8 +123,6 @@ new Vue({
|
|||
}, 10000)
|
||||
},
|
||||
onPaymentReceived: function (paymentHash) {
|
||||
this.fetchPayments()
|
||||
|
||||
if (this.receive.paymentHash === paymentHash) {
|
||||
this.receive.show = false
|
||||
this.receive.paymentHash = null
|
||||
|
@ -407,8 +169,6 @@ new Vue({
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.fetchPayments()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
|
@ -587,7 +347,6 @@ new Vue({
|
|||
this.parse.show = false
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
dismissPaymentMsg()
|
||||
this.fetchPayments()
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
|
@ -627,7 +386,6 @@ new Vue({
|
|||
if (res.data.paid) {
|
||||
dismissPaymentMsg()
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
this.fetchPayments()
|
||||
// show lnurlpay success action
|
||||
if (response.data.success_action) {
|
||||
switch (response.data.success_action.tag) {
|
||||
|
@ -740,27 +498,10 @@ new Vue({
|
|||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchPayments: function (props) {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||
return LNbits.api
|
||||
.getPayments(this.g.wallet, params)
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
fetchBalance: function () {
|
||||
LNbits.api.getWallet(this.g.wallet).then(response => {
|
||||
this.balance = Math.floor(response.data.balance / 1000)
|
||||
|
@ -785,31 +526,9 @@ new Vue({
|
|||
})
|
||||
.catch(e => console.error(e))
|
||||
},
|
||||
formatFiat(currency, amount) {
|
||||
return LNbits.utils.formatCurrency(amount, currency)
|
||||
},
|
||||
updateBalanceCallback: function (res) {
|
||||
this.balance += res.value
|
||||
},
|
||||
exportCSV: function () {
|
||||
// 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 query = {
|
||||
sortby: pagination.sortBy ?? 'time',
|
||||
direction: pagination.descending ? 'desc' : 'asc'
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.g.wallet, params).then(response => {
|
||||
const payments = response.data.data.map(LNbits.map.payment)
|
||||
LNbits.utils.exportCSV(
|
||||
this.paymentsCSV.columns,
|
||||
payments,
|
||||
this.g.wallet.name + '-payments'
|
||||
)
|
||||
})
|
||||
},
|
||||
pasteToTextArea: function () {
|
||||
this.$refs.textArea.focus() // Set cursor to textarea
|
||||
navigator.clipboard.readText().then(text => {
|
||||
|
@ -817,14 +536,6 @@ new Vue({
|
|||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
payments: function () {
|
||||
this.fetchBalance()
|
||||
},
|
||||
'paymentsChart.group': function () {
|
||||
this.showChart()
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
||||
|
@ -836,8 +547,6 @@ new Vue({
|
|||
if (this.$q.screen.lt.md) {
|
||||
this.mobileSimple = true
|
||||
}
|
||||
this.fetchPayments()
|
||||
|
||||
this.update.name = this.g.wallet.name
|
||||
this.update.currency = this.g.wallet.currency
|
||||
this.receive.units = ['sat', ...window.currencies]
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/components/payment-list.js",
|
||||
"js/components/payment-chart.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
|
|
|
@ -86,6 +86,8 @@
|
|||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/components/payment-list.js",
|
||||
"js/components/payment-chart.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
|
|
Loading…
Add table
Reference in a new issue