[feat] filter payments in wallet (#2997)

* fix: search payments
* fix rogue button
* feat: UI polishing
* feat: better charts

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
Vlad Stan 2025-02-26 12:34:57 +02:00 committed by GitHub
parent 5dc1705fa6
commit 5aa1f9b0f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 154 additions and 72 deletions

View file

@ -35,6 +35,7 @@
<q-table :rows="wallets" :columns="walletTable.columns"> <q-table :rows="wallets" :columns="walletTable.columns">
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width v-if="g.user.super_user"></q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th <q-th
auto-width auto-width
@ -47,12 +48,14 @@
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props"> <q-tr :props="props">
<q-td auto-width> <q-td auto-width v-if="g.user.super_user">
<lnbits-update-balance <lnbits-update-balance
:wallet_id="props.row.id" :wallet_id="props.row.id"
@credit-value="handleBalanceUpdate" @credit-value="handleBalanceUpdate"
class="q-mr-md" class="q-mr-md"
></lnbits-update-balance> ></lnbits-update-balance>
</q-td>
<q-td auto-width>
<q-btn <q-btn
round round
icon="menu" icon="menu"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -82,6 +82,7 @@ window.localisation.en = {
create_invoice: 'Create Invoice', create_invoice: 'Create Invoice',
camera_tooltip: 'Use camera to scan an invoice/QR', camera_tooltip: 'Use camera to scan an invoice/QR',
export_csv: 'Export to CSV', export_csv: 'Export to CSV',
export_csv_details: 'Export to CSV with details',
chart_tooltip: 'Show chart', chart_tooltip: 'Show chart',
pending: 'Pending', pending: 'Pending',
copy_invoice: 'Copy invoice', copy_invoice: 'Copy invoice',
@ -539,5 +540,7 @@ window.localisation.en = {
reset_wallet_keys_desc: reset_wallet_keys_desc:
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.', 'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
view_list: 'View wallets as list', view_list: 'View wallets as list',
view_column: 'View wallets as rows' view_column: 'View wallets as rows',
filter_payments: 'Filter payments',
filter_date: 'Filter by date'
} }

View file

@ -359,7 +359,7 @@ window.LNbits = {
prepareFilterQuery(tableConfig, props) { prepareFilterQuery(tableConfig, props) {
if (props) { if (props) {
tableConfig.pagination = props.pagination tableConfig.pagination = props.pagination
tableConfig.filter = {...tableConfig.filter, ...props.filter} Object.assign(tableConfig.filter, props.filter)
} }
const pagination = tableConfig.pagination const pagination = tableConfig.pagination
tableConfig.loading = true tableConfig.loading = true

View file

@ -595,7 +595,6 @@ window.app.component('username-password', {
}, },
async signInWithNostr() { async signInWithNostr() {
try { try {
console.log('### signInWithNostr')
const nostrToken = await this.createNostrToken() const nostrToken = await this.createNostrToken()
if (!nostrToken) { if (!nostrToken) {
return return

View file

@ -6,7 +6,6 @@ window.app.component('payment-list', {
data() { data() {
return { return {
denomination: LNBITS_DENOMINATION, denomination: LNBITS_DENOMINATION,
failedPaymentsToggle: false,
payments: [], payments: [],
paymentsTable: { paymentsTable: {
columns: [ columns: [
@ -39,6 +38,13 @@ window.app.component('payment-list', {
loading: false loading: false
}, },
searchDate: {from: null, to: null}, searchDate: {from: null, to: null},
searchStatus: {
success: true,
pending: true,
failed: false,
incoming: true,
outgoing: true
},
exportTagName: '', exportTagName: '',
exportPaymentTagList: [], exportPaymentTagList: [],
paymentsCSV: { paymentsCSV: {
@ -263,18 +269,40 @@ window.app.component('payment-list', {
console.error(e) console.error(e)
return `${amount} ???` return `${amount} ???`
} }
},
handleFilterChanged() {
const {success, pending, failed, incoming, outgoing} = this.searchStatus
delete this.paymentsTable.filter['status[ne]']
delete this.paymentsTable.filter['status[eq]']
if (success && pending && failed) {
// No status filter
} else if (success && pending) {
this.paymentsTable.filter['status[ne]'] = 'failed'
} else if (success && failed) {
this.paymentsTable.filter['status[ne]'] = 'pending'
} else if (failed && pending) {
this.paymentsTable.filter['status[ne]'] = 'success'
} else if (success) {
this.paymentsTable.filter['status[eq]'] = 'success'
} else if (pending) {
this.paymentsTable.filter['status[eq]'] = 'pending'
} else if (failed) {
this.paymentsTable.filter['status[eq]'] = 'failed'
}
delete this.paymentsTable.filter['amount[ge]']
delete this.paymentsTable.filter['amount[le]']
if (incoming && outgoing) {
// do nothing
} else if (incoming) {
this.paymentsTable.filter['amount[ge]'] = 0
} else if (outgoing) {
this.paymentsTable.filter['amount[le]'] = 0
}
} }
}, },
watch: { watch: {
failedPaymentsToggle(newVal) {
if (newVal === false) {
this.paymentsTable.filter['status[ne]'] = 'failed'
} else {
delete this.paymentsTable.filter['status[ne]']
}
this.paymentsTable.pagination.page = 1
this.fetchPayments()
},
'paymentsTable.search': { 'paymentsTable.search': {
handler() { handler() {
const props = {} const props = {}

View file

@ -352,9 +352,6 @@ window.UsersPageLogic = {
}) })
.catch(LNbits.utils.notifyApiError) .catch(LNbits.utils.notifyApiError)
}, },
exportUsers() {
console.log('export users')
},
async showAccountPage(user_id) { async showAccountPage(user_id) {
this.activeUser.showPassword = false this.activeUser.showPassword = false
this.activeUser.showUserId = false this.activeUser.showUserId = false

View file

@ -854,7 +854,9 @@ window.WalletPageLogic = {
handleFilterChange(value = {}) { handleFilterChange(value = {}) {
if ( if (
this.paymentsFilter['time[ge]'] !== value['time[ge]'] || this.paymentsFilter['time[ge]'] !== value['time[ge]'] ||
this.paymentsFilter['time[le]'] !== value['time[le]'] this.paymentsFilter['time[le]'] !== value['time[le]'] ||
this.paymentsFilter['amount[ge]'] !== value['amount[ge]'] ||
this.paymentsFilter['amount[le]'] !== value['amount[le]']
) { ) {
this.refreshCharts() this.refreshCharts()
} }
@ -884,6 +886,19 @@ window.WalletPageLogic = {
filterChartData(data) { filterChartData(data) {
const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00' const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00'
const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59' const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59'
let totalBalance = 0
data = data.map(p => {
if (this.paymentsFilter['amount[ge]'] !== undefined) {
totalBalance += p.balance_in
return {...p, balance: totalBalance, balance_out: 0, count_out: 0}
}
if (this.paymentsFilter['amount[le]'] !== undefined) {
totalBalance -= p.balance_out
return {...p, balance: totalBalance, balance_in: 0, count_in: 0}
}
return {...p}
})
data = data.filter(p => { data = data.filter(p => {
if ( if (
this.paymentsFilter['time[ge]'] && this.paymentsFilter['time[ge]'] &&
@ -899,6 +914,7 @@ window.WalletPageLogic = {
} }
return true return true
}) })
const labels = data.map(s => const labels = data.map(s =>
new Date(s.date).toLocaleString('default', { new Date(s.date).toLocaleString('default', {
month: 'short', month: 'short',

View file

@ -419,7 +419,6 @@
dense dense
use-input use-input
use-chips use-chips
multiple
hide-dropdown-icon hide-dropdown-icon
></q-select> ></q-select>
<div v-else-if="o.type === 'bool'"> <div v-else-if="o.type === 'bool'">
@ -639,16 +638,100 @@
</q-input> </q-input>
</div> </div>
<div class="gt-sm col-auto"> <div class="gt-sm col-auto">
<q-btn icon="event" flat color="grey">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
<div class="row">
<div class="col-6">
<q-btn
label="Search"
@click="searchByDate()"
color="primary"
flat
class="float-left"
v-close-popup
/>
</div>
<div class="col-6">
<q-btn
v-close-popup
@click="clearDateSeach()"
label="Clear"
class="float-right"
color="grey"
flat
/>
</div>
</div>
</q-popup-proxy>
<q-badge
v-if="searchDate?.to || searchDate?.from"
class="q-mt-lg q-mr-md"
color="primary"
rounded
floating
style="border-radius: 6px"
></q-badge>
<q-tooltip>
<span v-text="$t('filter_date')"></span>
</q-tooltip>
</q-btn>
<q-btn color="grey" icon="filter_alt" flat>
<q-menu>
<q-item dense>
<q-checkbox
v-model="searchStatus.success"
@click="handleFilterChanged"
label="Success Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="searchStatus.pending"
@click="handleFilterChanged"
label="Pending Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="searchStatus.failed"
@click="handleFilterChanged"
label="Failed Payments"
></q-checkbox>
</q-item>
<q-separator></q-separator>
<q-item dense>
<q-checkbox
v-model="searchStatus.incoming"
@click="handleFilterChanged"
label="Incoming Payments"
></q-checkbox>
</q-item>
<q-item dense>
<q-checkbox
v-model="searchStatus.outgoing"
@click="handleFilterChanged"
label="Outgoing Payments"
></q-checkbox>
</q-item>
</q-menu>
<q-tooltip>
<span v-text="$t('filter_payments')"></span>
</q-tooltip>
</q-btn>
<q-btn-dropdown <q-btn-dropdown
dense
outline outline
persistent persistent
dense icon="archive"
split
class="q-mr-sm" class="q-mr-sm"
color="grey" color="grey"
label="Export"
split
@click="exportCSV(false)" @click="exportCSV(false)"
> >
<q-tooltip>
<span v-text="$t('export_csv')"></span>
</q-tooltip>
<q-list> <q-list>
<q-item> <q-item>
<q-item-section> <q-item-section>
@ -688,59 +771,12 @@
outline outline
color="grey" color="grey"
@click="exportCSV(true)" @click="exportCSV(true)"
label="Export to CSV with details" :label="$t('export_csv_details')"
></q-btn> ></q-btn>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-btn icon="event" outline flat color="grey">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
<div class="row">
<div class="col-6">
<q-btn
label="Search"
@click="searchByDate()"
color="primary"
flat
class="float-left"
v-close-popup
/>
</div>
<div class="col-6">
<q-btn
v-close-popup
@click="clearDateSeach()"
label="Clear"
class="float-right"
color="grey"
flat
/>
</div>
</div>
</q-popup-proxy>
<q-badge
v-if="searchDate?.to || searchDate?.from"
class="q-mt-lg q-mr-md"
color="primary"
rounded
floating
style="border-radius: 6px"
/>
</q-btn>
<q-checkbox
v-model="failedPaymentsToggle"
checked-icon="warning"
unchecked-icon="warning_off"
:color="failedPaymentsToggle ? 'yellow' : 'grey'"
size="xs"
>
<q-tooltip>
<span v-text="`Include failed payments`"></span>
</q-tooltip>
</q-checkbox>
</div> </div>
</div> </div>
<div class="row q-my-md"></div> <div class="row q-my-md"></div>