feat(v2): Clients tab — LP management + settle balance modal (P9c)
Adds operator-scoped LP (liquidity provider) management. Operators
register LPs against specific machines, monitor remaining balances, and
settle small remainders via the P3e endpoint.
Template (Clients tab content + dialogs):
- Table with columns: machine, LP (username/short user_id), wallet,
DCA mode badge, remaining balance (color-coded: green if positive,
grey if zero), autoforward icon (with tooltip showing LN address),
status badge, action menu
- Empty-state banners: orange if no machines yet (LPs are
machine-scoped), blue if machines exist but no LPs registered
- Register-LP dialog: machine select + user_id + wallet_id + display
name + DCA mode (flow / fixed) + fixed-mode daily limit (conditional)
+ autoforward toggle + autoforward LN address (conditional)
- Edit-LP dialog: same minus immutable user_id/wallet_id, plus status
select (active/paused/closed)
- Settle-balance dialog (closes #4): funding wallet select + exchange
rate (operator-supplied) + optional amount_fiat (blank = full
remaining) + notes textarea. Shows the LP's current remaining
balance prominently before submission.
JS:
- loadClients pulls all operator's LPs across their fleet
- Per-LP balance summaries pre-loaded (one GET per LP — N+1, captured
in review issue #11 M3 for follow-up with a single grouped JOIN)
- openAddClientDialog / openEditClientDialog with separate cleaner
helpers (_cleanClientCreate vs _cleanClientUpdate) since the v2
API immutable-field rules differ between create and update
- openSettleBalanceDialog refreshes balance immediately before
showing the modal so the operator sees the up-to-date number
- confirmDeleteClient + DELETE wired
- machineNameById helper for displaying which machine an LP is at
- machineOptions computed for the register-LP machine select
- machinesById computed cache (avoids O(N*M) lookups in render loop)
Routes wired:
GET /api/v1/dca/clients
GET /api/v1/dca/clients/{id}/balance
POST /api/v1/dca/clients
PUT /api/v1/dca/clients/{id}
DELETE /api/v1/dca/clients/{id}
POST /api/v1/dca/clients/{id}/settle
Refs: aiolabs/satmachineadmin#9 — closes the Clients-tab gap in #4 +
exposes the autoforward setting (#8) in operator UI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13ac33047b
commit
0800a1acb0
2 changed files with 498 additions and 2 deletions
|
|
@ -17,6 +17,7 @@ const SUPER_FEE_PATH = `${API}/super-config`
|
|||
const MACHINES_PATH = `${API}/machines`
|
||||
const SETTLEMENTS_PATH = `${API}/settlements`
|
||||
const STUCK_PATH = `${API}/settlements/stuck`
|
||||
const CLIENTS_PATH = `${API}/clients`
|
||||
|
||||
const SETTLEMENT_STATUS_COLOR = {
|
||||
pending: 'grey',
|
||||
|
|
@ -40,6 +41,8 @@ window.app = Vue.createApp({
|
|||
// Server state ---------------------------------------------------
|
||||
superConfig: null,
|
||||
machines: [],
|
||||
clients: [],
|
||||
clientBalances: {}, // {client_id: ClientBalanceSummary}
|
||||
worklistCount: 0,
|
||||
|
||||
// UI configuration -----------------------------------------------
|
||||
|
|
@ -61,6 +64,25 @@ window.app = Vue.createApp({
|
|||
pagination: {rowsPerPage: 10, sortBy: 'name'}
|
||||
},
|
||||
|
||||
clientsTable: {
|
||||
columns: [
|
||||
{name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'},
|
||||
{name: 'username', label: 'LP', field: 'username', align: 'left'},
|
||||
{name: 'wallet_id', label: 'Wallet', field: 'wallet_id', align: 'left'},
|
||||
{name: 'dca_mode', label: 'Mode', field: 'dca_mode', align: 'left'},
|
||||
{
|
||||
name: 'remaining_balance',
|
||||
label: 'Balance',
|
||||
field: 'remaining_balance',
|
||||
align: 'right'
|
||||
},
|
||||
{name: 'autoforward', label: '→', field: 'autoforward_enabled', align: 'center'},
|
||||
{name: 'status', label: 'Status', field: 'status', align: 'left'},
|
||||
{name: 'actions', label: '', field: 'id', align: 'right'}
|
||||
],
|
||||
pagination: {rowsPerPage: 25}
|
||||
},
|
||||
|
||||
settlementsTable: {
|
||||
columns: [
|
||||
{name: 'status', label: 'Status', field: 'status', align: 'left'},
|
||||
|
|
@ -111,6 +133,24 @@ window.app = Vue.createApp({
|
|||
saving: false,
|
||||
settlement: null,
|
||||
note: ''
|
||||
},
|
||||
clientDialog: {
|
||||
show: false,
|
||||
saving: false,
|
||||
mode: 'add', // 'add' | 'edit'
|
||||
data: this._emptyClientForm()
|
||||
},
|
||||
settleBalanceDialog: {
|
||||
show: false,
|
||||
saving: false,
|
||||
client: null,
|
||||
balance: null,
|
||||
data: {
|
||||
funding_wallet_id: null,
|
||||
exchange_rate: null,
|
||||
amount_fiat: null,
|
||||
notes: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -120,6 +160,17 @@ window.app = Vue.createApp({
|
|||
// g.user is sometimes null on initial mount in LNbits 1.4 — guard it.
|
||||
const wallets = this.g?.user?.wallets || []
|
||||
return wallets.map(w => ({label: w.name, value: w.id}))
|
||||
},
|
||||
machineOptions() {
|
||||
return this.machines.map(m => ({
|
||||
label: m.name || this.shortNpub(m.machine_npub),
|
||||
value: m.id
|
||||
}))
|
||||
},
|
||||
machinesById() {
|
||||
const map = {}
|
||||
for (const m of this.machines) map[m.id] = m
|
||||
return map
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -137,6 +188,7 @@ window.app = Vue.createApp({
|
|||
await Promise.all([
|
||||
this.loadSuperConfig(),
|
||||
this.loadMachines(),
|
||||
this.loadClients(),
|
||||
this.loadWorklistCount()
|
||||
])
|
||||
} finally {
|
||||
|
|
@ -144,6 +196,38 @@ window.app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
|
||||
async loadClients() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', CLIENTS_PATH)
|
||||
this.clients = data || []
|
||||
// N+1 acceptable for fleet sizes ~50; review #11 captures the
|
||||
// single-grouped-JOIN follow-up (M3).
|
||||
await Promise.all(
|
||||
this.clients.map(c => this._loadClientBalance(c.id))
|
||||
)
|
||||
} catch (e) {
|
||||
this.clients = []
|
||||
this._notifyError(e, 'Failed to load LPs')
|
||||
}
|
||||
},
|
||||
|
||||
async _loadClientBalance(clientId) {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET', `${CLIENTS_PATH}/${clientId}/balance`
|
||||
)
|
||||
this.clientBalances[clientId] = data
|
||||
} catch (e) {
|
||||
delete this.clientBalances[clientId]
|
||||
}
|
||||
},
|
||||
|
||||
machineNameById(machineId) {
|
||||
const m = this.machinesById[machineId]
|
||||
if (!m) return this.shortId(machineId)
|
||||
return m.name || this.shortNpub(m.machine_npub)
|
||||
},
|
||||
|
||||
async loadSuperConfig() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', SUPER_FEE_PATH)
|
||||
|
|
@ -424,6 +508,125 @@ window.app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Client (LP) management (P9c)
|
||||
// -----------------------------------------------------------------
|
||||
openAddClientDialog() {
|
||||
this.clientDialog.mode = 'add'
|
||||
this.clientDialog.data = this._emptyClientForm()
|
||||
this.clientDialog.show = true
|
||||
},
|
||||
|
||||
openEditClientDialog(client) {
|
||||
this.clientDialog.mode = 'edit'
|
||||
this.clientDialog.data = {
|
||||
id: client.id,
|
||||
machine_id: client.machine_id,
|
||||
user_id: client.user_id,
|
||||
wallet_id: client.wallet_id,
|
||||
username: client.username || '',
|
||||
dca_mode: client.dca_mode,
|
||||
fixed_mode_daily_limit: client.fixed_mode_daily_limit,
|
||||
autoforward_enabled: !!client.autoforward_enabled,
|
||||
autoforward_ln_address: client.autoforward_ln_address || '',
|
||||
status: client.status
|
||||
}
|
||||
this.clientDialog.show = true
|
||||
},
|
||||
|
||||
async submitClient() {
|
||||
const d = this.clientDialog.data
|
||||
this.clientDialog.saving = true
|
||||
try {
|
||||
if (this.clientDialog.mode === 'add') {
|
||||
const body = this._cleanClientCreate(d)
|
||||
const {data} = await LNbits.api.request('POST', CLIENTS_PATH, null, body)
|
||||
this.clients.unshift(data)
|
||||
await this._loadClientBalance(data.id)
|
||||
Quasar.Notify.create({type: 'positive', message: 'LP registered'})
|
||||
} else {
|
||||
const body = this._cleanClientUpdate(d)
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT', `${CLIENTS_PATH}/${d.id}`, null, body
|
||||
)
|
||||
const idx = this.clients.findIndex(c => c.id === data.id)
|
||||
if (idx >= 0) this.clients[idx] = data
|
||||
Quasar.Notify.create({type: 'positive', message: 'LP updated'})
|
||||
}
|
||||
this.clientDialog.show = false
|
||||
} catch (e) {
|
||||
this._notifyError(e, 'Save failed')
|
||||
} finally {
|
||||
this.clientDialog.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteClient(client) {
|
||||
Quasar.Dialog.create({
|
||||
title: 'Delete LP?',
|
||||
message:
|
||||
`Remove <b>${client.username || this.shortId(client.user_id)}</b> from this machine. ` +
|
||||
'Their existing deposits and payment history are preserved — only the registration row goes. Continue?',
|
||||
html: true,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.request('DELETE', `${CLIENTS_PATH}/${client.id}`)
|
||||
this.clients = this.clients.filter(c => c.id !== client.id)
|
||||
delete this.clientBalances[client.id]
|
||||
Quasar.Notify.create({type: 'positive', message: 'LP deleted'})
|
||||
} catch (e) {
|
||||
this._notifyError(e, 'Delete failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Settle balance (P3e — closes #4)
|
||||
// -----------------------------------------------------------------
|
||||
async openSettleBalanceDialog(client) {
|
||||
this.settleBalanceDialog.client = client
|
||||
this.settleBalanceDialog.balance = this.clientBalances[client.id] || null
|
||||
this.settleBalanceDialog.data = {
|
||||
funding_wallet_id: null,
|
||||
exchange_rate: null,
|
||||
amount_fiat: null,
|
||||
notes: ''
|
||||
}
|
||||
// Refresh balance to make sure we're showing the latest before settling.
|
||||
await this._loadClientBalance(client.id)
|
||||
this.settleBalanceDialog.balance = this.clientBalances[client.id] || null
|
||||
this.settleBalanceDialog.show = true
|
||||
},
|
||||
|
||||
async submitSettleBalance() {
|
||||
const d = this.settleBalanceDialog
|
||||
const body = {
|
||||
funding_wallet_id: d.data.funding_wallet_id,
|
||||
exchange_rate: Number(d.data.exchange_rate),
|
||||
amount_fiat: d.data.amount_fiat ? Number(d.data.amount_fiat) : null,
|
||||
notes: d.data.notes || null
|
||||
}
|
||||
d.saving = true
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`${CLIENTS_PATH}/${d.client.id}/settle`,
|
||||
null,
|
||||
body
|
||||
)
|
||||
// Refresh this client's balance so the table reflects the new remaining.
|
||||
await this._loadClientBalance(d.client.id)
|
||||
d.show = false
|
||||
Quasar.Notify.create({type: 'positive', message: 'Balance settled'})
|
||||
} catch (e) {
|
||||
this._notifyError(e, 'Settle failed')
|
||||
} finally {
|
||||
d.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
_replaceSettlement(updated) {
|
||||
if (!updated) return
|
||||
const idx = this.machineDetail.settlements.findIndex(
|
||||
|
|
@ -470,6 +673,52 @@ window.app = Vue.createApp({
|
|||
})
|
||||
},
|
||||
|
||||
_emptyClientForm() {
|
||||
return {
|
||||
machine_id: null,
|
||||
user_id: '',
|
||||
wallet_id: '',
|
||||
username: '',
|
||||
dca_mode: 'flow',
|
||||
fixed_mode_daily_limit: null,
|
||||
autoforward_enabled: false,
|
||||
autoforward_ln_address: '',
|
||||
status: 'active'
|
||||
}
|
||||
},
|
||||
|
||||
_cleanClientCreate(d) {
|
||||
return {
|
||||
machine_id: d.machine_id,
|
||||
user_id: (d.user_id || '').trim(),
|
||||
wallet_id: (d.wallet_id || '').trim(),
|
||||
username: (d.username || '').trim() || null,
|
||||
dca_mode: d.dca_mode || 'flow',
|
||||
fixed_mode_daily_limit:
|
||||
d.dca_mode === 'fixed' && d.fixed_mode_daily_limit
|
||||
? Number(d.fixed_mode_daily_limit) : null,
|
||||
autoforward_enabled: !!d.autoforward_enabled,
|
||||
autoforward_ln_address:
|
||||
d.autoforward_enabled && d.autoforward_ln_address
|
||||
? d.autoforward_ln_address.trim() : null
|
||||
}
|
||||
},
|
||||
|
||||
_cleanClientUpdate(d) {
|
||||
return {
|
||||
username: (d.username || '').trim() || null,
|
||||
dca_mode: d.dca_mode,
|
||||
fixed_mode_daily_limit:
|
||||
d.dca_mode === 'fixed' && d.fixed_mode_daily_limit
|
||||
? Number(d.fixed_mode_daily_limit) : null,
|
||||
autoforward_enabled: !!d.autoforward_enabled,
|
||||
autoforward_ln_address:
|
||||
d.autoforward_enabled && d.autoforward_ln_address
|
||||
? d.autoforward_ln_address.trim() : null,
|
||||
status: d.status
|
||||
}
|
||||
},
|
||||
|
||||
_emptyMachineForm() {
|
||||
return {
|
||||
machine_npub: '',
|
||||
|
|
|
|||
|
|
@ -167,9 +167,112 @@
|
|||
<!-- PLACEHOLDERS for tabs that land in P9b–P9g -->
|
||||
<!-- ============================================================= -->
|
||||
<q-tab-panel name="clients">
|
||||
<q-banner class="bg-grey-2 text-grey-9">
|
||||
Clients tab — pending P9c.
|
||||
<div class="row items-center q-mb-md">
|
||||
<div class="col">
|
||||
<h6 class="q-my-none">Liquidity providers</h6>
|
||||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||
LPs receive proportional DCA distributions from your machines.
|
||||
Balances reflect deposits less the sats they've been paid.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" icon="person_add"
|
||||
label="Register LP"
|
||||
:disable="!machines.length"
|
||||
@click="openAddClientDialog" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="!machines.length" class="bg-orange-1 text-grey-9">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="orange" />
|
||||
</template>
|
||||
Register at least one machine before adding LPs — an LP is scoped
|
||||
to a specific machine.
|
||||
</q-banner>
|
||||
|
||||
<q-banner v-else-if="!clients.length" class="bg-blue-1 text-grey-9">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="info" color="blue" />
|
||||
</template>
|
||||
No LPs yet. Use <b>Register LP</b> to add one at any of your machines.
|
||||
</q-banner>
|
||||
|
||||
<q-table v-else
|
||||
dense flat
|
||||
:rows="clients"
|
||||
row-key="id"
|
||||
:columns="clientsTable.columns"
|
||||
:rows-per-page-options="[10, 25, 50]"
|
||||
:pagination="clientsTable.pagination">
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="machine">
|
||||
<span :style="{fontWeight: 500}"
|
||||
v-text="machineNameById(props.row.machine_id)"></span>
|
||||
</q-td>
|
||||
<q-td key="username">
|
||||
<span v-text="props.row.username || shortId(props.row.user_id)"></span>
|
||||
</q-td>
|
||||
<q-td key="wallet_id">
|
||||
<code :style="{fontSize: '0.8em'}"
|
||||
v-text="shortId(props.row.wallet_id)"></code>
|
||||
</q-td>
|
||||
<q-td key="dca_mode">
|
||||
<q-badge :color="props.row.dca_mode === 'flow' ? 'blue' : 'purple'"
|
||||
:label="props.row.dca_mode" />
|
||||
</q-td>
|
||||
<q-td key="remaining_balance" class="text-right">
|
||||
<span v-if="clientBalances[props.row.id]"
|
||||
:style="{color: clientBalances[props.row.id].remaining_balance > 0 ? '#2e7d32' : '#9e9e9e', fontWeight: 500}"
|
||||
v-text="formatFiat(clientBalances[props.row.id].remaining_balance, clientBalances[props.row.id].currency)"></span>
|
||||
<span v-else :style="{opacity: 0.5}">…</span>
|
||||
</q-td>
|
||||
<q-td key="autoforward">
|
||||
<q-icon v-if="props.row.autoforward_enabled"
|
||||
name="forward_to_inbox" color="primary" size="sm">
|
||||
<q-tooltip>
|
||||
Auto-forward enabled →
|
||||
<span v-text="props.row.autoforward_ln_address || '(no address)'"></span>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="forward" color="grey-5" size="sm">
|
||||
<q-tooltip>Auto-forward disabled</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="status">
|
||||
<q-badge
|
||||
:color="props.row.status === 'active' ? 'green' : 'grey'"
|
||||
:label="props.row.status" />
|
||||
</q-td>
|
||||
<q-td key="actions" auto-width>
|
||||
<q-btn-dropdown flat dense size="sm" icon="more_vert">
|
||||
<q-list dense>
|
||||
<q-item clickable v-close-popup
|
||||
@click="openEditClientDialog(props.row)">
|
||||
<q-item-section avatar><q-icon name="edit" /></q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup
|
||||
@click="openSettleBalanceDialog(props.row)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="payments" color="primary" />
|
||||
</q-item-section>
|
||||
<q-item-section>Settle balance…</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup
|
||||
@click="confirmDeleteClient(props.row)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete" color="red-7" />
|
||||
</q-item-section>
|
||||
<q-item-section>Delete</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="deposits">
|
||||
<q-banner class="bg-grey-2 text-grey-9">
|
||||
|
|
@ -523,6 +626,150 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- ADD / EDIT CLIENT DIALOGS -->
|
||||
<!-- =============================================================== -->
|
||||
<q-dialog v-model="clientDialog.show" persistent>
|
||||
<q-card :style="{minWidth: '520px', maxWidth: '95vw'}">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6" v-text="clientDialog.mode === 'add' ? 'Register liquidity provider' : 'Edit LP'"></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}"
|
||||
v-if="clientDialog.mode === 'add'">
|
||||
LPs receive DCA distributions proportional to their remaining
|
||||
balance. Each LP is scoped to a single machine; the same LP user
|
||||
can register at multiple machines as separate rows.
|
||||
</p>
|
||||
|
||||
<q-select
|
||||
v-if="clientDialog.mode === 'add'"
|
||||
v-model="clientDialog.data.machine_id"
|
||||
:options="machineOptions"
|
||||
label="At machine"
|
||||
emit-value map-options
|
||||
class="q-mb-md"
|
||||
dense outlined
|
||||
:rules="[v => !!v || 'Required']" />
|
||||
|
||||
<q-input v-if="clientDialog.mode === 'add'"
|
||||
v-model="clientDialog.data.user_id"
|
||||
label="LP's LNbits user_id"
|
||||
hint="The LP shares this from their LNbits account settings"
|
||||
class="q-mb-md" dense outlined
|
||||
:rules="[v => !!v || 'Required']" />
|
||||
|
||||
<q-input v-if="clientDialog.mode === 'add'"
|
||||
v-model="clientDialog.data.wallet_id"
|
||||
label="LP's wallet_id (receives DCA)"
|
||||
hint="The LP's wallet where their sats land"
|
||||
class="q-mb-md" dense outlined
|
||||
:rules="[v => !!v || 'Required']" />
|
||||
|
||||
<q-input v-model="clientDialog.data.username"
|
||||
label="Display name (optional)"
|
||||
class="q-mb-md" dense outlined />
|
||||
|
||||
<q-select v-model="clientDialog.data.dca_mode"
|
||||
:options="[{label: 'Flow (proportional)', value: 'flow'},
|
||||
{label: 'Fixed (daily limit)', value: 'fixed'}]"
|
||||
label="DCA mode"
|
||||
emit-value map-options
|
||||
class="q-mb-md" dense outlined />
|
||||
|
||||
<q-input v-if="clientDialog.data.dca_mode === 'fixed'"
|
||||
v-model.number="clientDialog.data.fixed_mode_daily_limit"
|
||||
label="Fixed mode daily limit (fiat)"
|
||||
type="number" step="0.01"
|
||||
class="q-mb-md" dense outlined />
|
||||
|
||||
<q-toggle v-model="clientDialog.data.autoforward_enabled"
|
||||
label="Auto-forward DCA to external LN address"
|
||||
class="q-mb-md" />
|
||||
|
||||
<q-input v-if="clientDialog.data.autoforward_enabled"
|
||||
v-model="clientDialog.data.autoforward_ln_address"
|
||||
label="LN address (e.g. user@walletofsatoshi.com)"
|
||||
hint="LP-controlled; failures leave sats safely in LP's LNbits wallet"
|
||||
class="q-mb-md" dense outlined />
|
||||
|
||||
<q-select v-if="clientDialog.mode === 'edit'"
|
||||
v-model="clientDialog.data.status"
|
||||
:options="['active', 'paused', 'closed']"
|
||||
label="Status"
|
||||
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="clientDialog.mode === 'add' ? 'Register' : 'Save'"
|
||||
:loading="clientDialog.saving"
|
||||
@click="submitClient" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- SETTLE BALANCE DIALOG (closes satmachineadmin#4) -->
|
||||
<!-- =============================================================== -->
|
||||
<q-dialog v-model="settleBalanceDialog.show" persistent>
|
||||
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Settle LP balance</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section v-if="settleBalanceDialog.client">
|
||||
<q-banner class="bg-blue-1 text-grey-9 q-mb-md">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="payments" color="blue" />
|
||||
</template>
|
||||
Pay the LP's remaining fiat balance in sats from your wallet at the
|
||||
rate you choose. Useful to zero out small balances that would
|
||||
otherwise shrink forever via proportional shares.
|
||||
</q-banner>
|
||||
|
||||
<div v-if="settleBalanceDialog.balance" class="q-mb-md">
|
||||
<div class="text-caption" :style="{opacity: 0.6}">Remaining balance</div>
|
||||
<div :style="{fontSize: '1.2em', fontWeight: 500}"
|
||||
v-text="formatFiat(settleBalanceDialog.balance.remaining_balance, settleBalanceDialog.balance.currency)"></div>
|
||||
</div>
|
||||
|
||||
<q-select v-model="settleBalanceDialog.data.funding_wallet_id"
|
||||
:options="walletOptions"
|
||||
label="Funding wallet (yours)"
|
||||
emit-value map-options
|
||||
class="q-mb-md" dense outlined
|
||||
:rules="[v => !!v || 'Pick a wallet']" />
|
||||
|
||||
<q-input v-model.number="settleBalanceDialog.data.exchange_rate"
|
||||
label="Exchange rate (sats per 1 fiat unit)"
|
||||
hint="You set the rate. Use exchange spot, midpoint, or a favourable gesture."
|
||||
type="number" step="0.0001" min="0"
|
||||
class="q-mb-md" dense outlined
|
||||
:rules="[v => v > 0 || 'Must be > 0']" />
|
||||
|
||||
<q-input v-model.number="settleBalanceDialog.data.amount_fiat"
|
||||
label="Amount (fiat) — leave blank to settle full remaining"
|
||||
type="number" step="0.01"
|
||||
class="q-mb-md" dense outlined />
|
||||
|
||||
<q-input v-model="settleBalanceDialog.data.notes"
|
||||
label="Notes (optional, audit memo)"
|
||||
type="textarea" autogrow
|
||||
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="Settle balance"
|
||||
:loading="settleBalanceDialog.saving"
|
||||
@click="submitSettleBalance" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- EDIT MACHINE DIALOG (same fields; PUT instead of POST) -->
|
||||
<!-- =============================================================== -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue