feat(v2): super-fee edit + Worklist + Reports (P9f+g, completes P9)
Combines the final three P9 pieces into a single commit since each is
small and they share the JS state plumbing.
Super-fee edit (P9f — visible only to super_user):
- "Edit" affordance on the platform-fee banner, gated on
g.user.super_user (LNbits passes this through windowMixin)
- Modal: super_fee_pct (decimal 0..1) + super_fee_wallet_id (text)
- PUT /api/v1/dca/super-config (check_super_user on the backend)
- Operators see the same banner read-only — no edit button rendered
Worklist tab (P9g part 1):
- Reuses GET /api/v1/dca/settlements/stuck?threshold_minutes=N
- Three labeled buckets: errored / stuck_pending / stuck_processing,
each with row count chip
- Per-row actions: open machine detail (reuses viewMachine), retry
(for errored), force-reset (for stuck — confirmation dialog warns
only-use-if-truly-stuck)
- Threshold input (default 30 min) + manual refresh button
- "All clear" green banner when worklist is empty
- Auto-loads on `created()` so the badge count is accurate from boot
Reports tab (P9g part 2):
- Four CSV download cards: machines / clients / deposits / payments
- Clients CSV merges in the per-LP balance summary from clientBalances
so the export captures total_deposits/payments/remaining + currency
- Payments CSV lazy-loads from GET /api/v1/dca/payments since payments
aren't cached in dashboard state (could be many rows)
- _downloadCsv helper properly quotes/escapes values with embedded
commas/quotes/newlines per RFC 4180
- All exports are client-side; no new endpoint required
P9 is now complete (P9a–P9g). The v1 super-only Lamassu dashboard is
fully replaced. Operators can register machines, manage LPs + deposits,
configure commission splits, work through errored settlements, and
export their data — all against the v2 backend.
Total v2 frontend: ~1326 lines JS + ~1349 lines template, replacing
the v1's 773 + 851. Increase is from the much larger v2 surface
(machines, leg-typed payments, commission editor, worklist, settle-
balance, partial-dispense, notes, force-reset, retry).
Refs: aiolabs/satmachineadmin#9 — completes P9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c8e629752
commit
f4eb7ec928
2 changed files with 420 additions and 7 deletions
|
|
@ -67,6 +67,40 @@ window.app = Vue.createApp({
|
||||||
// across the current legs (purely visual; doesn't hit the server).
|
// across the current legs (purely visual; doesn't hit the server).
|
||||||
commissionPreviewInput: 1000,
|
commissionPreviewInput: 1000,
|
||||||
|
|
||||||
|
// Worklist (P9g)
|
||||||
|
worklist: {
|
||||||
|
errored: [],
|
||||||
|
stuck_pending: [],
|
||||||
|
stuck_processing: [],
|
||||||
|
totalCount: 0
|
||||||
|
},
|
||||||
|
worklistLoading: false,
|
||||||
|
worklistThreshold: 30,
|
||||||
|
worklistTable: {
|
||||||
|
columns: [
|
||||||
|
{name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'},
|
||||||
|
{name: 'created_at', label: 'Created', field: 'created_at', align: 'left'},
|
||||||
|
{name: 'gross_sats', label: 'Gross', field: 'gross_sats', align: 'right'},
|
||||||
|
{
|
||||||
|
name: 'error_message',
|
||||||
|
label: 'Error',
|
||||||
|
field: 'error_message',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
{name: 'actions', label: '', field: 'id', align: 'right'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
reportsBusy: false,
|
||||||
|
|
||||||
|
// Super-fee edit dialog (super-only)
|
||||||
|
superFeeDialog: {
|
||||||
|
show: false,
|
||||||
|
saving: false,
|
||||||
|
data: {super_fee_pct: 0, super_fee_wallet_id: ''}
|
||||||
|
},
|
||||||
|
|
||||||
// UI configuration -----------------------------------------------
|
// UI configuration -----------------------------------------------
|
||||||
machinesTable: {
|
machinesTable: {
|
||||||
columns: [
|
columns: [
|
||||||
|
|
@ -226,6 +260,32 @@ window.app = Vue.createApp({
|
||||||
value: c.id
|
value: c.id
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
worklistBuckets() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'errored',
|
||||||
|
label: 'Errored — needs retry',
|
||||||
|
icon: 'error',
|
||||||
|
color: 'red',
|
||||||
|
rows: this.worklist.errored
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stuck_pending',
|
||||||
|
label: 'Stuck pending — listener crashed before processing?',
|
||||||
|
icon: 'hourglass_top',
|
||||||
|
color: 'orange',
|
||||||
|
rows: this.worklist.stuck_pending
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stuck_processing',
|
||||||
|
label: 'Stuck processing — processor crashed mid-flight?',
|
||||||
|
icon: 'sync_problem',
|
||||||
|
color: 'purple',
|
||||||
|
rows: this.worklist.stuck_processing
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
commissionScopeOptions() {
|
commissionScopeOptions() {
|
||||||
const opts = [{label: 'Default ruleset (operator-wide)', value: null}]
|
const opts = [{label: 'Default ruleset (operator-wide)', value: null}]
|
||||||
for (const m of this.machines) {
|
for (const m of this.machines) {
|
||||||
|
|
@ -284,6 +344,7 @@ window.app = Vue.createApp({
|
||||||
async created() {
|
async created() {
|
||||||
await this.refreshAll()
|
await this.refreshAll()
|
||||||
await this.loadCommissionSplits()
|
await this.loadCommissionSplits()
|
||||||
|
await this.loadWorklist()
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -377,8 +438,8 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadWorklistCount() {
|
async loadWorklistCount() {
|
||||||
// Light read used to badge the Worklist tab. The full worklist
|
// Light read for the tab badge — Worklist tab fetches the full
|
||||||
// panel lives in P9g; here we just count for the badge.
|
// payload via loadWorklist when opened.
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', STUCK_PATH)
|
const {data} = await LNbits.api.request('GET', STUCK_PATH)
|
||||||
this.worklistCount =
|
this.worklistCount =
|
||||||
|
|
@ -390,6 +451,162 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadWorklist() {
|
||||||
|
this.worklistLoading = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET', `${STUCK_PATH}?threshold_minutes=${this.worklistThreshold}`
|
||||||
|
)
|
||||||
|
this.worklist.errored = data?.errored || []
|
||||||
|
this.worklist.stuck_pending = data?.stuck_pending || []
|
||||||
|
this.worklist.stuck_processing = data?.stuck_processing || []
|
||||||
|
this.worklist.totalCount =
|
||||||
|
this.worklist.errored.length +
|
||||||
|
this.worklist.stuck_pending.length +
|
||||||
|
this.worklist.stuck_processing.length
|
||||||
|
this.worklistCount = this.worklist.totalCount
|
||||||
|
} catch (e) {
|
||||||
|
this._notifyError(e, 'Failed to load worklist')
|
||||||
|
} finally {
|
||||||
|
this.worklistLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async viewMachineFromWorklist(settlement) {
|
||||||
|
const machine = this.machinesById[settlement.machine_id]
|
||||||
|
if (!machine) return
|
||||||
|
await this.viewMachine(machine)
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmRetryFromWorklist(settlement) {
|
||||||
|
this.confirmRetrySettlement(settlement)
|
||||||
|
// Drop from worklist on success (optimistic; reload covers re-eval).
|
||||||
|
setTimeout(() => this.loadWorklist(), 500)
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmForceResetFromWorklist(settlement) {
|
||||||
|
this.confirmForceReset(settlement)
|
||||||
|
setTimeout(() => this.loadWorklist(), 500)
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Super-fee edit (P9f — super-only)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
openSuperFeeDialog() {
|
||||||
|
this.superFeeDialog.data = {
|
||||||
|
super_fee_pct: this.superConfig?.super_fee_pct ?? 0,
|
||||||
|
super_fee_wallet_id: this.superConfig?.super_fee_wallet_id || ''
|
||||||
|
}
|
||||||
|
this.superFeeDialog.show = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitSuperFee() {
|
||||||
|
const d = this.superFeeDialog.data
|
||||||
|
this.superFeeDialog.saving = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT', SUPER_FEE_PATH, null,
|
||||||
|
{
|
||||||
|
super_fee_pct: Number(d.super_fee_pct),
|
||||||
|
super_fee_wallet_id: (d.super_fee_wallet_id || '').trim() || null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.superConfig = data
|
||||||
|
this.superFeeDialog.show = false
|
||||||
|
Quasar.Notify.create({type: 'positive', message: 'Platform fee updated'})
|
||||||
|
} catch (e) {
|
||||||
|
this._notifyError(e, 'Save failed')
|
||||||
|
} finally {
|
||||||
|
this.superFeeDialog.saving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Reports / CSV exports (P9g)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
downloadMachinesCsv() {
|
||||||
|
this._downloadCsv(
|
||||||
|
'machines.csv',
|
||||||
|
['id', 'machine_npub', 'wallet_id', 'name', 'location', 'fiat_code',
|
||||||
|
'is_active', 'fallback_commission_pct', 'created_at'],
|
||||||
|
this.machines
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadClientsCsv() {
|
||||||
|
const rows = this.clients.map(c => {
|
||||||
|
const bal = this.clientBalances[c.id] || {}
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
machine_name: this.machineNameById(c.machine_id),
|
||||||
|
remaining_balance: bal.remaining_balance ?? '',
|
||||||
|
total_deposits: bal.total_deposits ?? '',
|
||||||
|
total_payments: bal.total_payments ?? '',
|
||||||
|
balance_currency: bal.currency ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._downloadCsv(
|
||||||
|
'clients.csv',
|
||||||
|
['id', 'machine_id', 'machine_name', 'user_id', 'wallet_id',
|
||||||
|
'username', 'dca_mode', 'status', 'autoforward_enabled',
|
||||||
|
'autoforward_ln_address', 'total_deposits', 'total_payments',
|
||||||
|
'remaining_balance', 'balance_currency', 'created_at'],
|
||||||
|
rows
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadDepositsCsv() {
|
||||||
|
this._downloadCsv(
|
||||||
|
'deposits.csv',
|
||||||
|
['id', 'client_id', 'machine_id', 'creator_user_id', 'amount',
|
||||||
|
'currency', 'status', 'notes', 'created_at', 'confirmed_at'],
|
||||||
|
this.deposits
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadPaymentsCsv() {
|
||||||
|
// Payments are not pre-loaded; fetch on demand.
|
||||||
|
this.reportsBusy = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request('GET', `${API}/payments`)
|
||||||
|
this._downloadCsv(
|
||||||
|
'payments.csv',
|
||||||
|
['id', 'settlement_id', 'client_id', 'machine_id', 'leg_type',
|
||||||
|
'destination_wallet_id', 'destination_ln_address', 'amount_sats',
|
||||||
|
'amount_fiat', 'exchange_rate', 'status', 'external_payment_hash',
|
||||||
|
'transaction_time', 'created_at', 'error_message'],
|
||||||
|
data || []
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
this._notifyError(e, 'Failed to fetch payments')
|
||||||
|
} finally {
|
||||||
|
this.reportsBusy = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_downloadCsv(filename, columns, rows) {
|
||||||
|
const escape = v => {
|
||||||
|
if (v == null) return ''
|
||||||
|
const s = String(v)
|
||||||
|
if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
const header = columns.join(',')
|
||||||
|
const body = rows.map(
|
||||||
|
row => columns.map(col => escape(row[col])).join(',')
|
||||||
|
).join('\n')
|
||||||
|
const csv = header + '\n' + body
|
||||||
|
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Add machine
|
// Add machine
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@
|
||||||
<span :style="{opacity: 0.7, fontSize: '0.85em'}" class="q-ml-sm">
|
<span :style="{opacity: 0.7, fontSize: '0.85em'}" class="q-ml-sm">
|
||||||
Your remainder splits per the rules below.
|
Your remainder splits per the rules below.
|
||||||
</span>
|
</span>
|
||||||
|
<template v-slot:action>
|
||||||
|
<q-btn v-if="g?.user?.super_user"
|
||||||
|
flat dense color="blue" icon="edit"
|
||||||
|
label="Edit"
|
||||||
|
@click="openSuperFeeDialog">
|
||||||
|
<q-tooltip>Super-only: set platform fee + destination wallet</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
<!-- Main tab strip -->
|
<!-- Main tab strip -->
|
||||||
|
|
@ -548,14 +556,167 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="worklist">
|
<q-tab-panel name="worklist">
|
||||||
<q-banner class="bg-grey-2 text-grey-9">
|
<div class="row items-center q-mb-md">
|
||||||
Worklist (stuck / errored settlements) — pending P9g.
|
<div class="col">
|
||||||
|
<h6 class="q-my-none">Worklist</h6>
|
||||||
|
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||||
|
Settlements that didn't process cleanly. Errored ones need
|
||||||
|
retry; stuck ones may need force-reset (processor crashed
|
||||||
|
mid-flight).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto row items-center q-gutter-sm">
|
||||||
|
<q-input v-model.number="worklistThreshold"
|
||||||
|
label="Threshold (min)" dense outlined
|
||||||
|
:style="{width: '120px'}"
|
||||||
|
type="number" min="1" />
|
||||||
|
<q-btn flat dense color="primary" icon="refresh"
|
||||||
|
:loading="worklistLoading"
|
||||||
|
@click="loadWorklist" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-banner v-if="worklist.totalCount === 0"
|
||||||
|
class="bg-green-1 text-grey-9">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="check_circle" color="green" />
|
||||||
|
</template>
|
||||||
|
All clear — no errored or stuck settlements.
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
|
<div v-for="bucket in worklistBuckets" :key="bucket.key"
|
||||||
|
v-show="bucket.rows.length"
|
||||||
|
class="q-mb-lg">
|
||||||
|
<div class="row items-center q-mb-sm">
|
||||||
|
<q-icon :name="bucket.icon" :color="bucket.color" size="sm"
|
||||||
|
class="q-mr-sm" />
|
||||||
|
<span :style="{fontWeight: 500}" v-text="bucket.label"></span>
|
||||||
|
<q-chip dense
|
||||||
|
:color="bucket.color" text-color="white"
|
||||||
|
class="q-ml-sm"
|
||||||
|
v-text="bucket.rows.length"></q-chip>
|
||||||
|
</div>
|
||||||
|
<q-table dense flat
|
||||||
|
:rows="bucket.rows"
|
||||||
|
row-key="id"
|
||||||
|
:columns="worklistTable.columns"
|
||||||
|
hide-pagination
|
||||||
|
:pagination="{rowsPerPage: 0}">
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="machine">
|
||||||
|
<span v-text="machineNameById(props.row.machine_id)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="created_at">
|
||||||
|
<span :style="{fontSize: '0.85em'}"
|
||||||
|
v-text="formatTime(props.row.created_at)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="gross_sats" class="text-right">
|
||||||
|
<span v-text="formatSats(props.row.gross_sats)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="error_message">
|
||||||
|
<span :style="{fontSize: '0.85em', opacity: 0.8}"
|
||||||
|
v-text="props.row.error_message || '—'"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="actions" auto-width>
|
||||||
|
<q-btn flat dense size="sm" icon="open_in_new"
|
||||||
|
color="primary"
|
||||||
|
@click="viewMachineFromWorklist(props.row)">
|
||||||
|
<q-tooltip>Open machine detail</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-if="bucket.key === 'errored'"
|
||||||
|
flat dense size="sm" icon="restart_alt"
|
||||||
|
color="primary"
|
||||||
|
@click="confirmRetryFromWorklist(props.row)">
|
||||||
|
<q-tooltip>Retry distribution</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-if="bucket.key !== 'errored'"
|
||||||
|
flat dense size="sm" icon="local_fire_department"
|
||||||
|
color="red"
|
||||||
|
@click="confirmForceResetFromWorklist(props.row)">
|
||||||
|
<q-tooltip>Force-reset (stuck)</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<q-tab-panel name="reports">
|
<q-tab-panel name="reports">
|
||||||
<q-banner class="bg-grey-2 text-grey-9">
|
<div class="row items-center q-mb-md">
|
||||||
Reports / CSV exports — pending P9g.
|
<div class="col">
|
||||||
</q-banner>
|
<h6 class="q-my-none">Reports</h6>
|
||||||
|
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||||
|
Client-side CSV exports of the data currently loaded in the
|
||||||
|
dashboard. For larger date ranges or server-side filters,
|
||||||
|
use the LNbits API directly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div :style="{fontWeight: 500}">Machines</div>
|
||||||
|
<div class="text-caption" :style="{opacity: 0.7}">
|
||||||
|
<span v-text="machines.length"></span> rows
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="primary" icon="download"
|
||||||
|
label="machines.csv"
|
||||||
|
@click="downloadMachinesCsv" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div :style="{fontWeight: 500}">Clients (LPs)</div>
|
||||||
|
<div class="text-caption" :style="{opacity: 0.7}">
|
||||||
|
<span v-text="clients.length"></span> rows, balances included
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="primary" icon="download"
|
||||||
|
label="clients.csv"
|
||||||
|
@click="downloadClientsCsv" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div :style="{fontWeight: 500}">Deposits</div>
|
||||||
|
<div class="text-caption" :style="{opacity: 0.7}">
|
||||||
|
<span v-text="deposits.length"></span> rows
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="primary" icon="download"
|
||||||
|
label="deposits.csv"
|
||||||
|
@click="downloadDepositsCsv" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-4">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div :style="{fontWeight: 500}">Payments (legs)</div>
|
||||||
|
<div class="text-caption" :style="{opacity: 0.7}">
|
||||||
|
Distribution audit (dca / super_fee / operator_split / etc)
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="primary" icon="download"
|
||||||
|
label="payments.csv"
|
||||||
|
:loading="reportsBusy"
|
||||||
|
@click="downloadPaymentsCsv" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
|
|
@ -889,6 +1050,41 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<!-- SUPER-FEE EDIT DIALOG (super-only) -->
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<q-dialog v-model="superFeeDialog.show" persistent>
|
||||||
|
<q-card :style="{minWidth: '460px', maxWidth: '95vw'}">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h6">Platform fee (super-only)</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p class="text-caption q-mb-md" :style="{opacity: 0.7}">
|
||||||
|
Charged on every operator's commission across the LNbits instance.
|
||||||
|
Operators see this as a read-only banner. Wallet ID is where the
|
||||||
|
collected fee lands; typically a wallet you (the super) own.
|
||||||
|
</p>
|
||||||
|
<q-input v-model.number="superFeeDialog.data.super_fee_pct"
|
||||||
|
label="Fee % (decimal, 0..1)"
|
||||||
|
hint="0.30 = 30% of every operator's commission"
|
||||||
|
type="number" step="0.0001" min="0" max="1"
|
||||||
|
class="q-mb-md" dense outlined />
|
||||||
|
<q-input v-model="superFeeDialog.data.super_fee_wallet_id"
|
||||||
|
label="Super fee destination wallet_id"
|
||||||
|
hint="LNbits wallet that collects the platform fee"
|
||||||
|
class="q-mb-md" dense outlined />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Cancel" v-close-popup />
|
||||||
|
<q-btn color="primary" label="Save"
|
||||||
|
:loading="superFeeDialog.saving"
|
||||||
|
@click="submitSuperFee" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<!-- =============================================================== -->
|
<!-- =============================================================== -->
|
||||||
<!-- ADD / EDIT DEPOSIT DIALOG -->
|
<!-- ADD / EDIT DEPOSIT DIALOG -->
|
||||||
<!-- =============================================================== -->
|
<!-- =============================================================== -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue