feat(v2): operator-side cassette inventory v1.1 + signer.nip44_* migration (#29) #30
2 changed files with 37 additions and 33 deletions
refactor(v2)(ui): denomination editable per slot, position read-only (#29 v1.1)
UI flips to position-keyed per the v1.1 redesign:
- Column order: Bay | Denomination | Count | ATM-reported | Updated
(position first, since it's the row identity)
- Position becomes read-only: rendered as "Bay N" label
- Denomination becomes an editable q-input (with the fiat code as a
suffix on the input)
- Count remains editable
- ATM-reported column now shows "<denom> <fiat> · ×<count>" combining
state_denomination + state_count for at-a-glance reconciliation
(still v1: only the bootstrap snapshot; v2 reverse-channel makes
this live)
- Confirm-modal preview list: header is "Bay N", side shows the
denomination + count being sent
JS:
- cassettesTable.columns reordered to put position first
- markCassetteDirty pivots on position (the immutable identity) and
compares denomination + count against pristine
- submitCassettePublish builds {positions: {<pos>: {denomination,
count}}} payload instead of {denominations: ...}
No "lock icon" on denomination — the previous instinct to add one was
based on the m007 misinterpretation. v1.1 design correctly makes
denomination operator-editable.
Tests still red until commit f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
3014962563
|
|
@ -205,10 +205,10 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
cassettesTable: {
|
cassettesTable: {
|
||||||
columns: [
|
columns: [
|
||||||
|
{name: 'position', label: 'Bay', field: 'position', align: 'right'},
|
||||||
{name: 'denomination', label: 'Denomination', field: 'denomination', align: 'right'},
|
{name: 'denomination', label: 'Denomination', field: 'denomination', align: 'right'},
|
||||||
{name: 'count', label: 'Count', field: 'count', align: 'right'},
|
{name: 'count', label: 'Count', field: 'count', align: 'right'},
|
||||||
{name: 'position', label: 'Position', field: 'position', align: 'right'},
|
{name: 'state', label: 'ATM-reported', field: 'state_denomination', align: 'right'},
|
||||||
{name: 'state_count', label: 'ATM-reported', field: 'state_count', align: 'right'},
|
|
||||||
{name: 'updated_at', label: 'Updated', field: 'updated_at', align: 'left'}
|
{name: 'updated_at', label: 'Updated', field: 'updated_at', align: 'left'}
|
||||||
],
|
],
|
||||||
pagination: {rowsPerPage: 0} // hide pagination — cassette count is small
|
pagination: {rowsPerPage: 0} // hide pagination — cassette count is small
|
||||||
|
|
@ -816,15 +816,16 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
markCassetteDirty(row) {
|
markCassetteDirty(row) {
|
||||||
// Find pristine match by denomination and compare; flip _dirty +
|
// Find pristine match by position (the row identity) and compare;
|
||||||
// overall dirty flag accordingly.
|
// flip _dirty + overall dirty flag accordingly. Editable fields
|
||||||
|
// are denomination + count; position is the immutable row key.
|
||||||
const pristine = this.machineDetail.cassettesPristine.find(
|
const pristine = this.machineDetail.cassettesPristine.find(
|
||||||
p => p.denomination === row.denomination
|
p => p.position === row.position
|
||||||
)
|
)
|
||||||
row._dirty =
|
row._dirty =
|
||||||
!pristine ||
|
!pristine ||
|
||||||
Number(row.count) !== Number(pristine.count) ||
|
Number(row.denomination) !== Number(pristine.denomination) ||
|
||||||
Number(row.position) !== Number(pristine.position)
|
Number(row.count) !== Number(pristine.count)
|
||||||
this.machineDetail.cassettesDirty =
|
this.machineDetail.cassettesDirty =
|
||||||
this.machineDetail.cassetteEdits.some(r => r._dirty)
|
this.machineDetail.cassetteEdits.some(r => r._dirty)
|
||||||
},
|
},
|
||||||
|
|
@ -844,18 +845,18 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitCassettePublish() {
|
async submitCassettePublish() {
|
||||||
// Build the PublishCassettesPayload shape:
|
// Build the PublishCassettesPayload shape (v1.1, position-keyed):
|
||||||
// { denominations: { "<denom>": { position, count }, ... } }
|
// { positions: { "<pos>": { denomination, count }, ... } }
|
||||||
// The API enforces the denomination set matches what's stored —
|
// The API enforces the position set matches what's stored —
|
||||||
// since we only edit existing rows, this should always pass.
|
// since we only edit existing rows, this should always pass.
|
||||||
const denominations = {}
|
const positions = {}
|
||||||
for (const row of this.machineDetail.cassetteEdits) {
|
for (const row of this.machineDetail.cassetteEdits) {
|
||||||
denominations[String(row.denomination)] = {
|
positions[String(row.position)] = {
|
||||||
position: Number(row.position),
|
denomination: Number(row.denomination),
|
||||||
count: Number(row.count)
|
count: Number(row.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const payload = {denominations}
|
const payload = {positions}
|
||||||
this.machineDetail.cassettesPublishing = true
|
this.machineDetail.cassettesPublishing = true
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
|
|
|
||||||
|
|
@ -1023,7 +1023,7 @@
|
||||||
<q-table v-if="machineDetail.cassetteEdits.length"
|
<q-table v-if="machineDetail.cassetteEdits.length"
|
||||||
dense flat
|
dense flat
|
||||||
:rows="machineDetail.cassetteEdits"
|
:rows="machineDetail.cassetteEdits"
|
||||||
row-key="denomination"
|
row-key="position"
|
||||||
:columns="cassettesTable.columns"
|
:columns="cassettesTable.columns"
|
||||||
:pagination="cassettesTable.pagination"
|
:pagination="cassettesTable.pagination"
|
||||||
hide-pagination>
|
hide-pagination>
|
||||||
|
|
@ -1032,10 +1032,15 @@
|
||||||
:style="props.row._dirty
|
:style="props.row._dirty
|
||||||
? {boxShadow: 'inset 4px 0 0 0 #fdd835'}
|
? {boxShadow: 'inset 4px 0 0 0 #fdd835'}
|
||||||
: {}">
|
: {}">
|
||||||
|
<q-td key="position" class="text-right">
|
||||||
|
<b v-text="'Bay ' + props.row.position"></b>
|
||||||
|
</q-td>
|
||||||
<q-td key="denomination" class="text-right">
|
<q-td key="denomination" class="text-right">
|
||||||
<b v-text="props.row.denomination"></b>
|
<q-input v-model.number="props.row.denomination"
|
||||||
<span :style="{fontSize: '0.85em', opacity: 0.6}"
|
type="number" min="1" step="1" dense outlined
|
||||||
v-text="' ' + (machineDetail.machine.fiat_code || '')"></span>
|
:suffix="machineDetail.machine.fiat_code || ''"
|
||||||
|
:style="{width: '140px', display: 'inline-block'}"
|
||||||
|
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="count" class="text-right">
|
<q-td key="count" class="text-right">
|
||||||
<q-input v-model.number="props.row.count" type="number"
|
<q-input v-model.number="props.row.count" type="number"
|
||||||
|
|
@ -1043,16 +1048,15 @@
|
||||||
:style="{width: '120px', display: 'inline-block'}"
|
:style="{width: '120px', display: 'inline-block'}"
|
||||||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="position" class="text-right">
|
<q-td key="state" class="text-right">
|
||||||
<q-input v-model.number="props.row.position" type="number"
|
<span v-if="props.row.state_denomination !== null"
|
||||||
min="1" step="1" dense outlined
|
:style="{fontSize: '0.85em', opacity: 0.7}">
|
||||||
:style="{width: '80px', display: 'inline-block'}"
|
<span v-text="props.row.state_denomination"></span>
|
||||||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
<span :style="{opacity: 0.6}"
|
||||||
</q-td>
|
v-text="' ' + (machineDetail.machine.fiat_code || '')"></span>
|
||||||
<q-td key="state_count" class="text-right">
|
<span :style="{opacity: 0.6}"> · </span>
|
||||||
<span v-if="props.row.state_count !== null"
|
<span v-text="'×' + props.row.state_count"></span>
|
||||||
:style="{fontSize: '0.85em', opacity: 0.7}"
|
</span>
|
||||||
v-text="props.row.state_count"></span>
|
|
||||||
<span v-else :style="{opacity: 0.4}">—</span>
|
<span v-else :style="{opacity: 0.4}">—</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="updated_at">
|
<q-td key="updated_at">
|
||||||
|
|
@ -1094,17 +1098,16 @@
|
||||||
<p class="q-mb-sm">Sending to ATM:</p>
|
<p class="q-mb-sm">Sending to ATM:</p>
|
||||||
<q-list dense bordered>
|
<q-list dense bordered>
|
||||||
<q-item v-for="row in machineDetail.cassetteEdits"
|
<q-item v-for="row in machineDetail.cassetteEdits"
|
||||||
:key="row.denomination">
|
:key="row.position">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>
|
||||||
<b v-text="row.denomination + ' ' +
|
<b v-text="'Bay ' + row.position"></b>
|
||||||
(machineDetail.machine.fiat_code || '')"></b>
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-item-label caption>
|
<q-item-label caption>
|
||||||
position
|
<b v-text="row.denomination + ' ' +
|
||||||
<b v-text="row.position"></b>
|
(machineDetail.machine.fiat_code || '')"></b>
|
||||||
· count
|
· count
|
||||||
<b v-text="row.count"></b>
|
<b v-text="row.count"></b>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue