feat(v2)(ui): cassette sub-tab in machine detail + overwrite-confirm modal (#29 v1)
The operator-facing surface for #29 v1. Two changes: 1. Sub-tab inside the existing machine-detail modal (`templates/satmachineadmin/index.html`): - q-tabs strip with Settlements + Cassettes inside the machine detail q-card-section, wrapping the existing Settlements content in a q-tab-panel name="settlements" and adding a new q-tab-panel name="cassettes" - Cassettes panel renders the cassette_configs rows from GET /api/v1/dca/machines/{id}/cassettes: - One row per denomination (read-only label) - Editable q-input for count + position (the only operator- editable fields per the locked design) - ATM-reported count column (read-only, shows the v2 reverse- channel state_count when populated; v1 only populates on bootstrap) - Last-updated timestamp - Dirty rows highlighted bg-yellow-1 - "Revert" + "Publish to ATM" buttons in the header; both disabled until at least one row is dirty - "Waiting for ATM bootstrap" banner when cassette_configs is empty (the bootstrap consumer hasn't received the ATM's state event yet) 2. Confirm-on-publish modal (per coord-log `07:50Z`): - Yellow warning banner: "This publish will overwrite the ATM's currently-tracked counts. If the ATM has dispensed cash since your last refill, those decrements will be lost. Publish only after a physical refill (a known total), not to 'tweak' counts mid-day. v2 reconciliation will replace this modal with reconciled state display." - Per-denomination preview list of what's being sent - Cancel + Publish-to-ATM buttons Vue 3 + Quasar UMD compliance per workspace CLAUDE.md: explicit-close tags (no self-closing), v-model.number on the numeric inputs, @update:model-value to trigger dirty-tracking, JSON-clone for the pristine snapshot. JS additions in `static/js/index.js`: - machineDetail.cassetteEdits / .cassettesPristine / .cassettesDirty / .cassettesLoading / .cassettesPublishing / .cassettesError state - cassettesTable.columns (no pagination — small fleets) - cassettePublishConfirm.show - loadMachineCassettes — fetches + sets pristine snapshot - markCassetteDirty — compares to pristine, toggles _dirty + the overall cassettesDirty flag - revertCassetteEdits — deep-clone pristine back into edits - openCassettePublishConfirm — opens the modal - submitCassettePublish — builds PublishCassettesPayload from edits, POSTs to /machines/{id}/cassettes/publish, refreshes from the response, closes modal on success, surfaces 400/503 errors in the inline banner reloadMachineDetail now also calls loadMachineCassettes so the Cassettes tab is pre-populated and tab-switching is flicker-free. viewMachine resets the cassette state (edits, pristine, dirty, error, activeTab) on each open. This is the final commit in the #29 v1 chain. PR #30 is ready for review once the build + manual smoke pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f8042f8e4d
commit
407149137a
2 changed files with 283 additions and 3 deletions
|
|
@ -818,7 +818,7 @@
|
|||
<q-btn flat dense round icon="refresh"
|
||||
@click="reloadMachineDetail"
|
||||
:loading="machineDetail.loading">
|
||||
<q-tooltip>Reload settlements</q-tooltip>
|
||||
<q-tooltip>Reload</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat dense round icon="close" v-close-popup>
|
||||
<q-tooltip>Close</q-tooltip>
|
||||
|
|
@ -845,7 +845,21 @@
|
|||
|
||||
<q-separator class="q-mb-md"></q-separator>
|
||||
|
||||
<div class="row items-center q-mb-sm">
|
||||
<q-tabs v-model="machineDetail.activeTab" dense
|
||||
align="left" class="text-grey-7"
|
||||
active-color="primary" indicator-color="primary"
|
||||
narrow-indicator>
|
||||
<q-tab name="settlements" icon="receipt_long"
|
||||
label="Settlements"></q-tab>
|
||||
<q-tab name="cassettes" icon="precision_manufacturing"
|
||||
label="Cassettes"></q-tab>
|
||||
</q-tabs>
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-tab-panels v-model="machineDetail.activeTab" animated>
|
||||
<q-tab-panel name="settlements" class="q-px-none">
|
||||
|
||||
<div class="row items-center q-mt-md q-mb-sm">
|
||||
<div class="col">
|
||||
<h6 class="q-my-none">Settlements</h6>
|
||||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||
|
|
@ -959,10 +973,153 @@
|
|||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="cassettes" class="q-px-none">
|
||||
|
||||
<div class="row items-center q-mt-md q-mb-sm">
|
||||
<div class="col">
|
||||
<h6 class="q-my-none">Cassettes</h6>
|
||||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||
Per-cassette count and physical bay position. Denomination
|
||||
set is hardware-determined (re-provision via atm-tui to
|
||||
change). "Publish to ATM" encrypts + signs + sends the new
|
||||
config to the machine via Nostr.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat dense icon="undo" label="Revert"
|
||||
:disable="!machineDetail.cassettesDirty"
|
||||
@click="revertCassetteEdits">
|
||||
<q-tooltip>Discard unsaved edits</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn color="primary" icon="cloud_upload"
|
||||
label="Publish to ATM"
|
||||
:disable="!machineDetail.cassettesDirty"
|
||||
:loading="machineDetail.cassettesPublishing"
|
||||
@click="openCassettePublishConfirm"></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="machineDetail.cassettesError"
|
||||
class="bg-red-1 text-grey-9 q-mb-md">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="negative"></q-icon>
|
||||
</template>
|
||||
<span v-text="machineDetail.cassettesError"></span>
|
||||
</q-banner>
|
||||
|
||||
<q-banner v-if="!machineDetail.cassetteEdits.length
|
||||
&& !machineDetail.cassettesLoading"
|
||||
class="bg-blue-1 text-grey-9">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="hourglass_empty" color="blue"></q-icon>
|
||||
</template>
|
||||
Waiting for the ATM's bootstrap state event. Power on the ATM
|
||||
and confirm it has reached the configured relay; cassette
|
||||
rows will auto-populate on receipt.
|
||||
</q-banner>
|
||||
|
||||
<q-table v-if="machineDetail.cassetteEdits.length"
|
||||
dense flat
|
||||
:rows="machineDetail.cassetteEdits"
|
||||
row-key="denomination"
|
||||
:columns="cassettesTable.columns"
|
||||
:pagination="cassettesTable.pagination"
|
||||
hide-pagination>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props"
|
||||
:class="props.row._dirty ? 'bg-yellow-1' : ''">
|
||||
<q-td key="denomination" class="text-right">
|
||||
<b v-text="props.row.denomination"></b>
|
||||
<span :style="{fontSize: '0.85em', opacity: 0.6}"
|
||||
v-text="' ' + (machineDetail.machine.fiat_code || '')"></span>
|
||||
</q-td>
|
||||
<q-td key="count" class="text-right">
|
||||
<q-input v-model.number="props.row.count" type="number"
|
||||
min="0" step="1" dense outlined
|
||||
:style="{width: '120px', display: 'inline-block'}"
|
||||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||||
</q-td>
|
||||
<q-td key="position" class="text-right">
|
||||
<q-input v-model.number="props.row.position" type="number"
|
||||
min="1" step="1" dense outlined
|
||||
:style="{width: '80px', display: 'inline-block'}"
|
||||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||||
</q-td>
|
||||
<q-td key="state_count" class="text-right">
|
||||
<span v-if="props.row.state_count !== null"
|
||||
:style="{fontSize: '0.85em', opacity: 0.7}"
|
||||
v-text="props.row.state_count"></span>
|
||||
<span v-else :style="{opacity: 0.4}">—</span>
|
||||
</q-td>
|
||||
<q-td key="updated_at">
|
||||
<span :style="{fontSize: '0.85em', opacity: 0.7}"
|
||||
v-text="formatTime(props.row.updated_at)"></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- CASSETTE PUBLISH CONFIRM DIALOG -->
|
||||
<!-- =============================================================== -->
|
||||
<q-dialog v-model="cassettePublishConfirm.show" persistent>
|
||||
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Publish cassette config to ATM</div>
|
||||
<q-space ></q-space>
|
||||
<q-btn icon="close" flat round dense v-close-popup></q-btn>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-banner class="bg-orange-1 text-grey-9 q-mb-md">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="warning"></q-icon>
|
||||
</template>
|
||||
<b>This publish will overwrite the ATM's currently-tracked
|
||||
counts.</b> If the ATM has dispensed cash since your last
|
||||
refill or count baseline, those decrements will be lost.
|
||||
Publish only after a physical refill (a known total), not to
|
||||
"tweak" counts mid-day. v2 reconciliation will replace this
|
||||
modal with reconciled state display.
|
||||
</q-banner>
|
||||
<p class="q-mb-sm">Sending to ATM:</p>
|
||||
<q-list dense bordered>
|
||||
<q-item v-for="row in machineDetail.cassetteEdits"
|
||||
:key="row.denomination">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<b v-text="row.denomination + ' ' +
|
||||
(machineDetail.machine.fiat_code || '')"></b>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label caption>
|
||||
position
|
||||
<b v-text="row.position"></b>
|
||||
· count
|
||||
<b v-text="row.count"></b>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||
<q-btn color="primary"
|
||||
label="Publish to ATM"
|
||||
:loading="machineDetail.cassettesPublishing"
|
||||
@click="submitCassettePublish"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- PARTIAL-DISPENSE DIALOG -->
|
||||
<!-- =============================================================== -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue