From 4dfdb167d5c71f8a03666c441cd17bc6c6ecc0ce Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 17 Jul 2026 09:04:52 -0400 Subject: [PATCH] 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. --- docs/PLUGIN-LAB-PRINTEDPARTS.md | 14 +++++++ .../src/views/printedparts/PartsKiosk.vue | 41 ++++++++++++++----- plugins/printedparts/api/routes.py | 26 +++++++++--- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/docs/PLUGIN-LAB-PRINTEDPARTS.md b/docs/PLUGIN-LAB-PRINTEDPARTS.md index be58d98..d6e868b 100644 --- a/docs/PLUGIN-LAB-PRINTEDPARTS.md +++ b/docs/PLUGIN-LAB-PRINTEDPARTS.md @@ -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/` 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) diff --git a/frontend/src/views/printedparts/PartsKiosk.vue b/frontend/src/views/printedparts/PartsKiosk.vue index 66a31f0..5da0e85 100644 --- a/frontend/src/views/printedparts/PartsKiosk.vue +++ b/frontend/src/views/printedparts/PartsKiosk.vue @@ -19,12 +19,17 @@

Scan the barcode on the bin

No scanner? - Type the code + Type the number

-
- - +
+
{{ manualCode || 'label number' }}
+ + +

Just the number from the label - e.g. 4 for + {{ '0004' }}; letters are added automatically.

@@ -37,11 +42,14 @@

{{ item.itemcode }} - {{ item.quantityonhand }} on hand

-

Scan your badge

-
- - +

Scan your badge or tap in your SSO

+
+
{{ manualBadge || 'SSO' }}
+ +
@@ -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; +} diff --git a/plugins/printedparts/api/routes.py b/plugins/printedparts/api/routes.py index 83720b2..4c07931 100644 --- a/plugins/printedparts/api/routes.py +++ b/plugins/printedparts/api/routes.py @@ -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/', 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)