feat(v2): Deposits tab — record/confirm/reject workflow (P9d)

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) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 18:05:25 +02:00
commit ce4d7e4dd6
2 changed files with 436 additions and 2 deletions

View file

@ -275,9 +275,138 @@
</q-table>
</q-tab-panel>
<q-tab-panel name="deposits">
<q-banner class="bg-grey-2 text-grey-9">
Deposits tab — pending P9d.
<div class="row items-center q-mb-md">
<div class="col">
<h6 class="q-my-none">Deposits</h6>
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
Record fiat handed in by LPs. Confirmed deposits increase the
LP's balance and feed proportional DCA distribution.
</p>
</div>
<div class="col-auto">
<q-btn color="primary" icon="add"
label="Record deposit"
:disable="!clients.length"
@click="openAddDepositDialog" />
</div>
</div>
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-12 col-md-3">
<q-select v-model="depositsFilter.status"
:options="[
{label: 'All statuses', value: null},
{label: 'Pending', value: 'pending'},
{label: 'Confirmed', value: 'confirmed'},
{label: 'Rejected', value: 'rejected'}]"
label="Status" emit-value map-options dense outlined />
</div>
<div class="col-12 col-md-4">
<q-select v-model="depositsFilter.client_id"
:options="depositClientOptions"
label="LP" emit-value map-options dense outlined clearable />
</div>
</div>
<q-banner v-if="!clients.length" class="bg-orange-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="warning" color="orange" />
</template>
Register at least one LP before recording deposits.
</q-banner>
<q-banner v-else-if="!filteredDeposits.length && !deposits.length"
class="bg-blue-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
</template>
No deposits yet. Use <b>Record deposit</b> to log a new one.
</q-banner>
<q-banner v-else-if="!filteredDeposits.length"
class="bg-grey-2 text-grey-9">
No deposits match the current filters.
</q-banner>
<q-table v-else
dense flat
:rows="filteredDeposits"
row-key="id"
:columns="depositsTable.columns"
:rows-per-page-options="[10, 25, 50]"
:pagination="depositsTable.pagination">
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="status">
<q-badge :color="depositStatusColor(props.row.status)"
:label="props.row.status" />
</q-td>
<q-td key="client">
<span v-text="clientUsernameById(props.row.client_id)"></span>
<div :style="{fontSize: '0.75em', opacity: 0.6}"
v-text="machineNameById(props.row.machine_id)"></div>
</q-td>
<q-td key="amount" class="text-right">
<span :style="{fontWeight: 500}"
v-text="formatFiat(props.row.amount, props.row.currency)"></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="confirmed_at">
<span v-if="props.row.confirmed_at"
:style="{fontSize: '0.85em'}"
v-text="formatTime(props.row.confirmed_at)"></span>
<span v-else :style="{opacity: 0.4}"></span>
</q-td>
<q-td key="notes">
<span v-if="props.row.notes"
:style="{fontSize: '0.85em'}"
v-text="props.row.notes"></span>
<span v-else :style="{opacity: 0.4}"></span>
</q-td>
<q-td key="actions" auto-width>
<q-btn-dropdown flat dense size="sm" icon="more_vert">
<q-list dense>
<q-item v-if="props.row.status === 'pending'"
clickable v-close-popup
@click="confirmDepositStatus(props.row, 'confirmed')">
<q-item-section avatar>
<q-icon name="check_circle" color="green" />
</q-item-section>
<q-item-section>Confirm</q-item-section>
</q-item>
<q-item v-if="props.row.status === 'pending'"
clickable v-close-popup
@click="openRejectDepositDialog(props.row)">
<q-item-section avatar>
<q-icon name="cancel" color="red" />
</q-item-section>
<q-item-section>Reject…</q-item-section>
</q-item>
<q-item v-if="props.row.status === 'pending'"
clickable v-close-popup
@click="openEditDepositDialog(props.row)">
<q-item-section avatar>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item v-if="props.row.status === 'pending'"
clickable v-close-popup
@click="confirmDeleteDeposit(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="commission">
<q-banner class="bg-grey-2 text-grey-9">
@ -626,6 +755,80 @@
</q-card>
</q-dialog>
<!-- =============================================================== -->
<!-- ADD / EDIT DEPOSIT DIALOG -->
<!-- =============================================================== -->
<q-dialog v-model="depositDialog.show" persistent>
<q-card :style="{minWidth: '460px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6"
v-text="depositDialog.mode === 'add' ? 'Record deposit' : 'Edit deposit'"></div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section>
<q-select v-if="depositDialog.mode === 'add'"
v-model="depositDialog.data.client_id"
:options="depositClientOptions"
label="Liquidity provider"
emit-value map-options
class="q-mb-md" dense outlined
:rules="[v => !!v || 'Pick an LP']" />
<q-input v-model.number="depositDialog.data.amount"
label="Amount (fiat)"
type="number" step="0.01" min="0"
class="q-mb-md" dense outlined
:rules="[v => v > 0 || 'Must be > 0']" />
<q-input v-model="depositDialog.data.currency"
label="Currency"
class="q-mb-md" dense outlined />
<q-input v-model="depositDialog.data.notes"
label="Notes (optional)"
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="depositDialog.mode === 'add' ? 'Record' : 'Save'"
:loading="depositDialog.saving"
@click="submitDeposit" />
</q-card-actions>
</q-card>
</q-dialog>
<!-- =============================================================== -->
<!-- REJECT DEPOSIT DIALOG (status update with notes) -->
<!-- =============================================================== -->
<q-dialog v-model="rejectDepositDialog.show" persistent>
<q-card :style="{minWidth: '420px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Reject deposit</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-sm" :style="{opacity: 0.7}">
The deposit will be marked rejected and won't count toward the LP's
balance. Optional reason for the audit trail.
</p>
<q-input v-model="rejectDepositDialog.notes"
label="Reason (optional)"
type="textarea" autogrow
dense outlined />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn color="red" label="Reject"
:loading="rejectDepositDialog.saving"
@click="submitRejectDeposit" />
</q-card-actions>
</q-card>
</q-dialog>
<!-- =============================================================== -->
<!-- ADD / EDIT CLIENT DIALOGS -->
<!-- =============================================================== -->