From ce4d7e4dd6aa70219d78c4234652f2a231b7a7ea Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 14 May 2026 18:05:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(v2):=20Deposits=20tab=20=E2=80=94=20record?= =?UTF-8?q?/confirm/reject=20workflow=20(P9d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator records fiat handed in by LPs and drives the pending → confirmed status transition that promotes deposits into LP balances. Template (Deposits tab content + dialogs): - Filter strip: status dropdown (all/pending/confirmed/rejected) and LP dropdown (filtered by all the operator's LPs across machines) - Table columns: status badge, LP (+ machine subtitle), amount, created at, confirmed at, notes, action menu - Action menu (only enabled on pending status — confirmed/rejected are immutable for audit): • Confirm — flips to status='confirmed' + refreshes LP balance • Reject — opens reject dialog for optional reason notes • Edit — amount/currency/notes change • Delete - Empty-state banners: orange if no LPs (deposits are LP-scoped), blue if LPs exist but no deposits yet, grey if filters return nothing - Record-deposit dialog: LP select (auto-derives machine), amount, currency, notes - Edit-deposit dialog: amount/currency/notes; LP+machine immutable - Reject-deposit dialog: optional reason text persisted with the status JS: - loadDeposits, depositStatusColor, clientUsernameById helpers - depositClientOptions computed: includes machine name in each option label so operators see exactly where the deposit will land - filteredDeposits computed: client-side filter on the loaded list (no server-side filter param — operator's deposit volume small enough) - submitDeposit handles both create and update paths; the create body explicitly includes machine_id (auto-derived from the selected LP) so the server can cross-check (client_id, machine_id) alignment - confirmDepositStatus refreshes the LP balance after confirming, since the confirmed deposit now affects remaining_balance display Routes wired: GET /api/v1/dca/deposits POST /api/v1/dca/deposits PUT /api/v1/dca/deposits/{id} PUT /api/v1/dca/deposits/{id}/status DELETE /api/v1/dca/deposits/{id} Refs: aiolabs/satmachineadmin#9 Co-Authored-By: Claude Opus 4.7 (1M context) --- static/js/index.js | 231 +++++++++++++++++++++++++++ templates/satmachineadmin/index.html | 207 +++++++++++++++++++++++- 2 files changed, 436 insertions(+), 2 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 084eec3..73c8525 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -18,6 +18,13 @@ const MACHINES_PATH = `${API}/machines` const SETTLEMENTS_PATH = `${API}/settlements` const STUCK_PATH = `${API}/settlements/stuck` const CLIENTS_PATH = `${API}/clients` +const DEPOSITS_PATH = `${API}/deposits` + +const DEPOSIT_STATUS_COLOR = { + pending: 'orange', + confirmed: 'green', + rejected: 'red' +} const SETTLEMENT_STATUS_COLOR = { pending: 'grey', @@ -43,8 +50,14 @@ window.app = Vue.createApp({ machines: [], clients: [], clientBalances: {}, // {client_id: ClientBalanceSummary} + deposits: [], worklistCount: 0, + depositsFilter: { + status: null, + client_id: null + }, + // UI configuration ----------------------------------------------- machinesTable: { columns: [ @@ -64,6 +77,24 @@ window.app = Vue.createApp({ pagination: {rowsPerPage: 10, sortBy: 'name'} }, + depositsTable: { + columns: [ + {name: 'status', label: 'Status', field: 'status', align: 'left'}, + {name: 'client', label: 'LP / Machine', field: 'client_id', align: 'left'}, + {name: 'amount', label: 'Amount', field: 'amount', align: 'right'}, + {name: 'created_at', label: 'Created', field: 'created_at', align: 'left'}, + { + name: 'confirmed_at', + label: 'Confirmed', + field: 'confirmed_at', + align: 'left' + }, + {name: 'notes', label: 'Notes', field: 'notes', align: 'left'}, + {name: 'actions', label: '', field: 'id', align: 'right'} + ], + pagination: {rowsPerPage: 25, sortBy: 'created_at', descending: true} + }, + clientsTable: { columns: [ {name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'}, @@ -140,6 +171,19 @@ window.app = Vue.createApp({ mode: 'add', // 'add' | 'edit' data: this._emptyClientForm() }, + depositDialog: { + show: false, + saving: false, + mode: 'add', + data: this._emptyDepositForm() + }, + rejectDepositDialog: { + show: false, + saving: false, + deposit: null, + notes: '' + }, + settleBalanceDialog: { show: false, saving: false, @@ -167,6 +211,22 @@ window.app = Vue.createApp({ value: m.id })) }, + depositClientOptions() { + return this.clients.map(c => ({ + label: `${c.username || this.shortId(c.user_id)} @ ${this.machineNameById(c.machine_id)}`, + value: c.id + })) + }, + filteredDeposits() { + let rows = this.deposits + if (this.depositsFilter.status) { + rows = rows.filter(d => d.status === this.depositsFilter.status) + } + if (this.depositsFilter.client_id) { + rows = rows.filter(d => d.client_id === this.depositsFilter.client_id) + } + return rows + }, machinesById() { const map = {} for (const m of this.machines) map[m.id] = m @@ -189,6 +249,7 @@ window.app = Vue.createApp({ this.loadSuperConfig(), this.loadMachines(), this.loadClients(), + this.loadDeposits(), this.loadWorklistCount() ]) } finally { @@ -196,6 +257,26 @@ window.app = Vue.createApp({ } }, + async loadDeposits() { + try { + const {data} = await LNbits.api.request('GET', DEPOSITS_PATH) + this.deposits = data || [] + } catch (e) { + this.deposits = [] + this._notifyError(e, 'Failed to load deposits') + } + }, + + depositStatusColor(status) { + return DEPOSIT_STATUS_COLOR[status] || 'grey' + }, + + clientUsernameById(clientId) { + const c = this.clients.find(x => x.id === clientId) + if (!c) return this.shortId(clientId) + return c.username || this.shortId(c.user_id) + }, + async loadClients() { try { const {data} = await LNbits.api.request('GET', CLIENTS_PATH) @@ -582,6 +663,147 @@ window.app = Vue.createApp({ }) }, + // ----------------------------------------------------------------- + // Deposits (P9d) + // ----------------------------------------------------------------- + openAddDepositDialog() { + this.depositDialog.mode = 'add' + this.depositDialog.data = this._emptyDepositForm() + this.depositDialog.show = true + }, + + openEditDepositDialog(deposit) { + this.depositDialog.mode = 'edit' + this.depositDialog.data = { + id: deposit.id, + client_id: deposit.client_id, + amount: deposit.amount, + currency: deposit.currency, + notes: deposit.notes || '' + } + this.depositDialog.show = true + }, + + async submitDeposit() { + const d = this.depositDialog.data + this.depositDialog.saving = true + try { + if (this.depositDialog.mode === 'add') { + // machine_id is server-cross-checked but we send it explicitly. + const client = this.clients.find(c => c.id === d.client_id) + if (!client) throw new Error('client not found') + const body = { + client_id: d.client_id, + machine_id: client.machine_id, + amount: Number(d.amount), + currency: (d.currency || 'GTQ').trim(), + notes: (d.notes || '').trim() || null + } + const {data} = await LNbits.api.request('POST', DEPOSITS_PATH, null, body) + this.deposits.unshift(data) + Quasar.Notify.create({type: 'positive', message: 'Deposit recorded'}) + } else { + const body = { + amount: Number(d.amount), + currency: (d.currency || 'GTQ').trim(), + notes: (d.notes || '').trim() || null + } + const {data} = await LNbits.api.request( + 'PUT', `${DEPOSITS_PATH}/${d.id}`, null, body + ) + this._replaceDeposit(data) + Quasar.Notify.create({type: 'positive', message: 'Deposit updated'}) + } + this.depositDialog.show = false + } catch (e) { + this._notifyError(e, 'Save failed') + } finally { + this.depositDialog.saving = false + } + }, + + confirmDepositStatus(deposit, newStatus) { + const verb = newStatus === 'confirmed' ? 'Confirm' : 'Reject' + Quasar.Dialog.create({ + title: `${verb} deposit?`, + message: + newStatus === 'confirmed' + ? `Confirming will count this toward the LP's DCA balance.` + : `Rejecting marks it ignored; it won't affect balances.`, + cancel: true, + persistent: true + }).onOk(async () => { + try { + const {data} = await LNbits.api.request( + 'PUT', + `${DEPOSITS_PATH}/${deposit.id}/status`, + null, + {status: newStatus, notes: deposit.notes || null} + ) + this._replaceDeposit(data) + // Confirming changes the LP balance — refresh it. + if (newStatus === 'confirmed') { + await this._loadClientBalance(deposit.client_id) + } + Quasar.Notify.create({ + type: 'positive', + message: `Deposit ${newStatus}` + }) + } catch (e) { + this._notifyError(e, 'Status update failed') + } + }) + }, + + openRejectDepositDialog(deposit) { + this.rejectDepositDialog.deposit = deposit + this.rejectDepositDialog.notes = '' + this.rejectDepositDialog.show = true + }, + + async submitRejectDeposit() { + const d = this.rejectDepositDialog + d.saving = true + try { + const {data} = await LNbits.api.request( + 'PUT', + `${DEPOSITS_PATH}/${d.deposit.id}/status`, + null, + {status: 'rejected', notes: d.notes || null} + ) + this._replaceDeposit(data) + d.show = false + Quasar.Notify.create({type: 'positive', message: 'Deposit rejected'}) + } catch (e) { + this._notifyError(e, 'Reject failed') + } finally { + d.saving = false + } + }, + + confirmDeleteDeposit(deposit) { + Quasar.Dialog.create({ + title: 'Delete deposit?', + message: 'Only pending deposits can be deleted.', + cancel: true, + persistent: true + }).onOk(async () => { + try { + await LNbits.api.request('DELETE', `${DEPOSITS_PATH}/${deposit.id}`) + this.deposits = this.deposits.filter(x => x.id !== deposit.id) + Quasar.Notify.create({type: 'positive', message: 'Deposit deleted'}) + } catch (e) { + this._notifyError(e, 'Delete failed') + } + }) + }, + + _replaceDeposit(updated) { + if (!updated) return + const idx = this.deposits.findIndex(d => d.id === updated.id) + if (idx >= 0) this.deposits[idx] = updated + }, + // ----------------------------------------------------------------- // Settle balance (P3e — closes #4) // ----------------------------------------------------------------- @@ -673,6 +895,15 @@ window.app = Vue.createApp({ }) }, + _emptyDepositForm() { + return { + client_id: null, + amount: null, + currency: 'GTQ', + notes: '' + } + }, + _emptyClientForm() { return { machine_id: null, diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html index 2688181..db05a71 100644 --- a/templates/satmachineadmin/index.html +++ b/templates/satmachineadmin/index.html @@ -275,9 +275,138 @@ - - Deposits tab — pending P9d. +
+
+
Deposits
+

+ Record fiat handed in by LPs. Confirmed deposits increase the + LP's balance and feed proportional DCA distribution. +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + Register at least one LP before recording deposits. + + + + No deposits yet. Use Record deposit to log a new one. + + + + No deposits match the current filters. + + + + +
@@ -626,6 +755,80 @@ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+
+ + + + + + + +
Reject deposit
+ + +
+ +

+ The deposit will be marked rejected and won't count toward the LP's + balance. Optional reason for the audit trail. +

+ +
+ + + + +
+
+