1 Commits

Author SHA1 Message Date
cproudlock
4dfdb167d5 printedparts stage 16: kiosk touch fixes from first hands-on use
Some checks failed
CI / backend (push) Successful in 1m44s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s
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.
2026-07-17 09:04:52 -04:00
3 changed files with 64 additions and 17 deletions

View File

@@ -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
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)

View File

@@ -19,12 +19,17 @@
<p class="kiosk-prompt">Scan the barcode on the bin</p>
<p class="kiosk-hint">
No scanner?
<a href="#" @click.prevent="manualEntry = !manualEntry">Type the code</a>
<a href="#" @click.prevent="manualEntry = !manualEntry">Type the number</a>
</p>
<div v-if="manualEntry" class="manual-row">
<input v-model="manualCode" class="form-control" placeholder="3DP-0001"
@keydown.enter="lookupItem(manualCode)" />
<button class="btn btn-primary" @click="lookupItem(manualCode)">Go</button>
<div v-if="manualEntry" class="manual-block">
<div class="quantity-display">{{ manualCode || 'label number' }}</div>
<TouchKeypad @digit="manualCode += $event"
@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>
</section>
@@ -37,11 +42,14 @@
<p class="kiosk-hint">{{ item.itemcode }} - {{ item.quantityonhand }} on hand</p>
</div>
</div>
<p class="kiosk-prompt">Scan your badge</p>
<div class="manual-row">
<input v-model="manualBadge" class="form-control" placeholder="or type your SSO"
@keydown.enter="acceptBadge(manualBadge)" />
<button class="btn btn-primary" @click="acceptBadge(manualBadge)">Next</button>
<p class="kiosk-prompt">Scan your badge or tap in your SSO</p>
<div class="manual-block">
<div class="quantity-display">{{ manualBadge || 'SSO' }}</div>
<TouchKeypad @digit="manualBadge += $event"
@clear="manualBadge = ''"
@backspace="manualBadge = manualBadge.slice(0, -1)" />
<button class="btn btn-primary take-button" :disabled="!manualBadge"
@click="acceptBadge(manualBadge)">Next</button>
</div>
</section>
@@ -96,7 +104,12 @@ let resetTimer = null
onMounted(focusWedge)
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()
}
@@ -239,4 +252,10 @@ function reset() {
padding: 0.9rem 3.5rem;
}
.manual-row { display: flex; gap: 0.6rem; }
.manual-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
</style>

View File

@@ -381,12 +381,28 @@ def adjust_item(item_id: int):
# 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.
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'])
def kiosk_item(itemcode):
"""Item summary for a scanned bin barcode (open read for the kiosk)."""
item = PrintedItem.query.filter(
PrintedItem.itemcode == itemcode.strip(),
PrintedItem.isactive == True).first()
item = _kiosk_find_item(itemcode)
if not item:
return error_response(ErrorCodes.NOT_FOUND,
'No part matches that barcode', http_code=404)
@@ -398,9 +414,7 @@ def kiosk_take():
"""Take parts from a bin. Body: {itemcode, badge, quantity}."""
data = request.get_json() or {}
item = PrintedItem.query.filter(
PrintedItem.itemcode == (data.get('itemcode') or '').strip(),
PrintedItem.isactive == True).first()
item = _kiosk_find_item(data.get('itemcode'))
if not item:
return error_response(ErrorCodes.NOT_FOUND,
'No part matches that barcode', http_code=404)