printedparts stage 16: kiosk touch fixes from first hands-on use
The tap-anywhere wedge refocus stole focus from the manual-entry field the moment it was tapped - the handler now only reclaims focus from dead space, never from a real control. Manual entry works without a physical keyboard: badge entry uses the TouchKeypad (an SSO is digits), and item lookup accepts bare digits resolved by row id - the digits in a minted code are the id, which also keeps labels printed under an older prefix scannable after the prefix changes.
This commit is contained in:
@@ -371,6 +371,20 @@ Two more field requests, and the plugin's FIRST incremental migration:
|
|||||||
ACTIVE member of each selected role (role.users backref), deduped with
|
ACTIVE member of each selected role (role.users backref), deduped with
|
||||||
the user picks and free-text; settings page gains a role picker.
|
the user picks and free-text; settings page gains a role picker.
|
||||||
|
|
||||||
|
## Stage 16 (extension) - kiosk touch fixes from first hands-on use
|
||||||
|
|
||||||
|
First real touchscreen session found two problems worth their own stage:
|
||||||
|
1. Focus steal: the page's tap-anywhere handler refocused the hidden wedge
|
||||||
|
input, yanking focus out of the manual-entry field the moment it was
|
||||||
|
tapped. Guard the handler - never reclaim focus from INPUT/SELECT/
|
||||||
|
TEXTAREA/BUTTON/A targets, only from dead space.
|
||||||
|
2. No physical keyboard on a touchscreen: manual fallbacks now use the
|
||||||
|
TouchKeypad. Badge entry is digits (an SSO) so the keypad covers it;
|
||||||
|
item codes are letters+digits, solved server-side instead of building an
|
||||||
|
alphanumeric keyboard - the digits in a minted code ARE the row id, so
|
||||||
|
`/kiosk/item/<digits>` resolves bare digits by id. Bonus: labels printed
|
||||||
|
under an older code prefix keep working after the prefix changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Where each pattern lives (cheat sheet)
|
## Where each pattern lives (cheat sheet)
|
||||||
|
|||||||
@@ -19,12 +19,17 @@
|
|||||||
<p class="kiosk-prompt">Scan the barcode on the bin</p>
|
<p class="kiosk-prompt">Scan the barcode on the bin</p>
|
||||||
<p class="kiosk-hint">
|
<p class="kiosk-hint">
|
||||||
No scanner?
|
No scanner?
|
||||||
<a href="#" @click.prevent="manualEntry = !manualEntry">Type the code</a>
|
<a href="#" @click.prevent="manualEntry = !manualEntry">Type the number</a>
|
||||||
</p>
|
</p>
|
||||||
<div v-if="manualEntry" class="manual-row">
|
<div v-if="manualEntry" class="manual-block">
|
||||||
<input v-model="manualCode" class="form-control" placeholder="3DP-0001"
|
<div class="quantity-display">{{ manualCode || 'label number' }}</div>
|
||||||
@keydown.enter="lookupItem(manualCode)" />
|
<TouchKeypad @digit="manualCode += $event"
|
||||||
<button class="btn btn-primary" @click="lookupItem(manualCode)">Go</button>
|
@clear="manualCode = ''"
|
||||||
|
@backspace="manualCode = manualCode.slice(0, -1)" />
|
||||||
|
<button class="btn btn-primary take-button" :disabled="!manualCode"
|
||||||
|
@click="lookupItem(manualCode)">Look up</button>
|
||||||
|
<p class="kiosk-hint">Just the number from the label - e.g. 4 for
|
||||||
|
{{ '0004' }}; letters are added automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -37,11 +42,14 @@
|
|||||||
<p class="kiosk-hint">{{ item.itemcode }} - {{ item.quantityonhand }} on hand</p>
|
<p class="kiosk-hint">{{ item.itemcode }} - {{ item.quantityonhand }} on hand</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="kiosk-prompt">Scan your badge</p>
|
<p class="kiosk-prompt">Scan your badge or tap in your SSO</p>
|
||||||
<div class="manual-row">
|
<div class="manual-block">
|
||||||
<input v-model="manualBadge" class="form-control" placeholder="or type your SSO"
|
<div class="quantity-display">{{ manualBadge || 'SSO' }}</div>
|
||||||
@keydown.enter="acceptBadge(manualBadge)" />
|
<TouchKeypad @digit="manualBadge += $event"
|
||||||
<button class="btn btn-primary" @click="acceptBadge(manualBadge)">Next</button>
|
@clear="manualBadge = ''"
|
||||||
|
@backspace="manualBadge = manualBadge.slice(0, -1)" />
|
||||||
|
<button class="btn btn-primary take-button" :disabled="!manualBadge"
|
||||||
|
@click="acceptBadge(manualBadge)">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -96,7 +104,12 @@ let resetTimer = null
|
|||||||
onMounted(focusWedge)
|
onMounted(focusWedge)
|
||||||
onBeforeUnmount(() => clearTimeout(resetTimer))
|
onBeforeUnmount(() => clearTimeout(resetTimer))
|
||||||
|
|
||||||
function focusWedge() {
|
function focusWedge(event) {
|
||||||
|
// Tapping a visible input/button must keep it - only reclaim focus for
|
||||||
|
// the wedge scanner from dead space.
|
||||||
|
const tag = event?.target?.tagName
|
||||||
|
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA'
|
||||||
|
|| tag === 'BUTTON' || tag === 'A') return
|
||||||
wedgeInput.value?.focus()
|
wedgeInput.value?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,4 +252,10 @@ function reset() {
|
|||||||
padding: 0.9rem 3.5rem;
|
padding: 0.9rem 3.5rem;
|
||||||
}
|
}
|
||||||
.manual-row { display: flex; gap: 0.6rem; }
|
.manual-row { display: flex; gap: 0.6rem; }
|
||||||
|
.manual-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -381,12 +381,28 @@ def adjust_item(item_id: int):
|
|||||||
# physically rate-limited. It can reduce stock of an active item and nothing
|
# physically rate-limited. It can reduce stock of an active item and nothing
|
||||||
# else; identity comes from the badge resolved server-side, never the client.
|
# else; identity comes from the badge resolved server-side, never the client.
|
||||||
|
|
||||||
|
def _kiosk_find_item(itemcode):
|
||||||
|
"""Resolve a scanned or typed code to an active item.
|
||||||
|
|
||||||
|
Accepts the full code (WJRP0042) or bare digits from the touch keypad
|
||||||
|
(42 -> prefix + zero-pad), so manual entry never needs letters."""
|
||||||
|
itemcode = (itemcode or '').strip()
|
||||||
|
item = PrintedItem.query.filter(
|
||||||
|
PrintedItem.itemcode == itemcode,
|
||||||
|
PrintedItem.isactive == True).first()
|
||||||
|
if not item and itemcode.isdigit():
|
||||||
|
# The digits in a minted code ARE the row id, so id lookup keeps
|
||||||
|
# working even for labels printed under an older prefix.
|
||||||
|
candidate = db.session.get(PrintedItem, int(itemcode))
|
||||||
|
if candidate and candidate.isactive:
|
||||||
|
item = candidate
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
@printedparts_bp.route('/kiosk/item/<itemcode>', methods=['GET'])
|
@printedparts_bp.route('/kiosk/item/<itemcode>', methods=['GET'])
|
||||||
def kiosk_item(itemcode):
|
def kiosk_item(itemcode):
|
||||||
"""Item summary for a scanned bin barcode (open read for the kiosk)."""
|
"""Item summary for a scanned bin barcode (open read for the kiosk)."""
|
||||||
item = PrintedItem.query.filter(
|
item = _kiosk_find_item(itemcode)
|
||||||
PrintedItem.itemcode == itemcode.strip(),
|
|
||||||
PrintedItem.isactive == True).first()
|
|
||||||
if not item:
|
if not item:
|
||||||
return error_response(ErrorCodes.NOT_FOUND,
|
return error_response(ErrorCodes.NOT_FOUND,
|
||||||
'No part matches that barcode', http_code=404)
|
'No part matches that barcode', http_code=404)
|
||||||
@@ -398,9 +414,7 @@ def kiosk_take():
|
|||||||
"""Take parts from a bin. Body: {itemcode, badge, quantity}."""
|
"""Take parts from a bin. Body: {itemcode, badge, quantity}."""
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
|
|
||||||
item = PrintedItem.query.filter(
|
item = _kiosk_find_item(data.get('itemcode'))
|
||||||
PrintedItem.itemcode == (data.get('itemcode') or '').strip(),
|
|
||||||
PrintedItem.isactive == True).first()
|
|
||||||
if not item:
|
if not item:
|
||||||
return error_response(ErrorCodes.NOT_FOUND,
|
return error_response(ErrorCodes.NOT_FOUND,
|
||||||
'No part matches that barcode', http_code=404)
|
'No part matches that barcode', http_code=404)
|
||||||
|
|||||||
Reference in New Issue
Block a user