From 052780c8654821d8cedf032448e11ef0cf7b9ba3 Mon Sep 17 00:00:00 2001 From: swdee Date: Sat, 17 Jan 2026 17:52:20 +1300 Subject: [PATCH] removed augmented checkbox and combined functionality into info mode checkbox. changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found. --- .../pages/barcode_scan_controller.js | 135 ++++++++---------- src/Controller/ScanController.php | 104 ++++++++------ src/Form/LabelSystem/ScanDialogType.php | 5 - .../scanner/augmented_result.html.twig | 15 +- .../label_system/scanner/scanner.html.twig | 2 +- translations/messages.en.xlf | 12 +- 6 files changed, 137 insertions(+), 136 deletions(-) diff --git a/assets/controllers/pages/barcode_scan_controller.js b/assets/controllers/pages/barcode_scan_controller.js index c8aeac80..b5a96834 100644 --- a/assets/controllers/pages/barcode_scan_controller.js +++ b/assets/controllers/pages/barcode_scan_controller.js @@ -29,12 +29,20 @@ export default class extends Controller { _scanner = null; _submitting = false; _lastDecodedText = ""; + _onInfoChange = null; connect() { // Prevent double init if connect fires twice if (this._scanner) return; - this.bindModeToggles(); + // clear last decoded barcode when state changes on info box + const info = document.getElementById("scan_dialog_info_mode"); + if (info) { + this._onInfoChange = () => { + this._lastDecodedText = ""; + }; + info.addEventListener("change", this._onInfoChange); + } const isMobile = window.matchMedia("(max-width: 768px)").matches; @@ -74,7 +82,13 @@ export default class extends Controller { this._scanner = null; this._submitting = false; this._lastDecodedText = ""; - this.unbindModeToggles(); + + // Unbind info-mode change handler (always do this, even if scanner is null) + const info = document.getElementById("scan_dialog_info_mode"); + if (info && this._onInfoChange) { + info.removeEventListener("change", this._onInfoChange); + } + this._onInfoChange = null; if (!scanner) return; @@ -86,49 +100,14 @@ export default class extends Controller { } } - /** - * Add events to Mode checkboxes so they both can't be selected at the same time - */ - bindModeToggles() { - const info = document.getElementById("scan_dialog_info_mode"); - const aug = document.getElementById("scan_dialog_augmented_mode"); - if (!info || !aug) return; - - const onInfoChange = () => { - if (info.checked) aug.checked = false; - }; - const onAugChange = () => { - if (aug.checked) info.checked = false; - }; - - info.addEventListener("change", onInfoChange); - aug.addEventListener("change", onAugChange); - - // Save references so we can remove listeners on disconnect - this._onInfoChange = onInfoChange; - this._onAugChange = onAugChange; - } - - unbindModeToggles() { - const info = document.getElementById("scan_dialog_info_mode"); - const aug = document.getElementById("scan_dialog_augmented_mode"); - if (!info || !aug) return; - - if (this._onInfoChange) info.removeEventListener("change", this._onInfoChange); - if (this._onAugChange) aug.removeEventListener("change", this._onAugChange); - - this._onInfoChange = null; - this._onAugChange = null; - } - - async onScanSuccess(decodedText) { if (!decodedText) return; const normalized = String(decodedText).trim(); + if (!normalized) return; - // If we already handled this exact barcode and it's still showing, ignore. + // scan once per barcode if (normalized === this._lastDecodedText) return; // If a request/submit is in-flight, ignore scans. @@ -142,43 +121,42 @@ export default class extends Controller { const input = document.getElementById("scan_dialog_input"); if (input) input.value = decodedText; - const augmented = !!document.getElementById("scan_dialog_augmented_mode")?.checked; + const infoMode = !!document.getElementById("scan_dialog_info_mode")?.checked; - // If augmented mode: do NOT submit the form. - if (augmented) { - try { - await this.lookupAndRender(decodedText); - } catch (e) { - console.warn("[barcode_scan] augmented lookup failed", e); - // Allow retry on failure by clearing last decoded text - this._lastDecodedText = ""; - } finally { - // allow scanning again - this._submitting = false; - } - return; - } - - // Non-augmented: Stop scanner BEFORE submitting to avoid camera transition races try { - if (this._scanner?.clear) { - await this._scanner.clear(); - } - } catch (_) { - // ignore - } finally { - this._scanner = null; - } + const data = await this.lookup(normalized, infoMode); - //Submit form - document.getElementById("scan_dialog_form")?.requestSubmit(); + // ok:false = transient junk decode; ignore without wiping UI + if (!data || data.ok !== true) { + this._lastDecodedText = ""; // allow retry + return; + } + + // If info mode is OFF and part was found -> redirect + if (!infoMode && data.found && data.redirectUrl) { + window.location.assign(data.redirectUrl); + return; + } + + // Otherwise render returned fragment HTML + if (typeof data.html === "string" && data.html !== "") { + const el = document.getElementById("scan-augmented-result"); + if (el) el.innerHTML = data.html; + } + } catch (e) { + console.warn("[barcode_scan] lookup failed", e); + // allow retry on failure + this._lastDecodedText = ""; + } finally { + this._submitting = false; + } } - async lookupAndRender(decodedText) { - const form = document.getElementById("scan_dialog_form"); - if (!form) return; - // Ensure the hidden csrf field has been converted from placeholder -> real token + cookie set + async lookup(decodedText, infoMode) { + const form = document.getElementById("scan_dialog_form"); + if (!form) return { ok: false }; + generateCsrfToken(form); const mode = @@ -187,23 +165,28 @@ export default class extends Controller { const body = new URLSearchParams(); body.set("input", decodedText); if (mode !== "") body.set("mode", mode); + body.set("info_mode", infoMode ? "1" : "0"); const headers = { - "Accept": "text/html", + "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", - ...generateCsrfHeaders(form), // adds the special CSRF header Symfony expects (if enabled) + ...generateCsrfHeaders(form), }; - const resp = await fetch(this.element.dataset.augmentedUrl, { + const url = this.element.dataset.lookupUrl; + if (!url) throw new Error("Missing data-lookup-url on #reader-box"); + + const resp = await fetch(url, { method: "POST", headers, body: body.toString(), credentials: "same-origin", }); - const html = await resp.text(); + if (!resp.ok) { + throw new Error(`lookup failed: HTTP ${resp.status}`); + } - const el = document.getElementById("scan-augmented-result"); - if (el) el.innerHTML = html; + return await resp.json(); } } diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index cc5e0bf2..8676095f 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -83,46 +83,39 @@ class ScanController extends AbstractController $form = $this->createForm(ScanDialogType::class); $form->handleRequest($request); - $mode = null; + // If JS is working, scanning uses /scan/lookup and this action just renders the page. + // This fallback only runs if user submits the form manually or uses ?input=... if ($input === null && $form->isSubmitted() && $form->isValid()) { $input = $form['input']->getData(); - $mode = $form['mode']->getData(); } $infoModeData = null; - $createUrl = null; - if ($input !== null) { + if ($input !== null && $input !== '') { + $mode = $form->isSubmitted() ? $form['mode']->getData() : null; + $infoMode = $form->isSubmitted() ? (bool) $form['info_mode']->getData() : false; + try { - $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); + $scan = $this->barcodeNormalizer->scanBarcodeContent((string) $input, $mode ?? null); - //Perform a redirect if the info mode is not enabled - if (!$form['info_mode']->getData()) { - try { - // redirect user to part page - return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); - } catch (EntityNotFoundException) { - // Part not found -> show decoded info + optional "create part" link - $infoModeData = $scan_result->getDecodedForInfoMode(); - - $createUrl = $this->buildCreateUrlForScanResult($scan_result, $request->getLocale()); - - if ($createUrl === null) { - $this->addFlash('warning', 'scan.qr_not_found'); - } - } - } else { //Otherwise retrieve infoModeData - $infoModeData = $scan_result->getDecodedForInfoMode(); + // If not in info mode, mimic “normal scan” behavior: redirect if possible. + if (!$infoMode) { + $url = $this->barcodeParser->getRedirectURL($scan); + return $this->redirect($url); } - } catch (InvalidArgumentException) { - $this->addFlash('error', 'scan.format_unknown'); + + // Info mode fallback: render page with prefilled result + $infoModeData = $scan->getDecodedForInfoMode(); + + } catch (\Throwable $e) { + // Keep fallback user-friendly; avoid 500 + $this->addFlash('warning', 'scan.format_unknown'); } } return $this->render('label_system/scanner/scanner.html.twig', [ 'form' => $form, 'infoModeData' => $infoModeData, - 'createUrl' => $createUrl, ]); } @@ -295,64 +288,81 @@ class ScanController extends AbstractController return array_reverse($items); } - #[Route(path: '/augmented', name: 'scan_augmented', methods: ['POST'])] - public function augmented(Request $request): Response + /** + * Provides XHR endpoint for looking up barcode information and return JSON response + * @param Request $request + * @return JsonResponse + */ + #[Route(path: '/lookup', name: 'scan_lookup', methods: ['POST'])] + public function lookup(Request $request): JsonResponse { $this->denyAccessUnlessGranted('@tools.label_scanner'); - $input = (string) $request->request->get('input', ''); - $mode = $request->request->get('mode'); // string|null + $input = trim((string) $request->request->get('input', '')); + $mode = (string) ($request->request->get('mode') ?? ''); + $infoMode = (bool) filter_var($request->request->get('info_mode', false), FILTER_VALIDATE_BOOL); + $locale = $request->getLocale(); if ($input === '') { - // Return empty fragment or an error fragment; your choice - return new Response('', 200); + return new JsonResponse(['ok' => false], 200); } $modeEnum = null; - if ($mode !== null && $mode !== '') { - // Radio values are enum integers in your form + if ($mode !== '') { $modeEnum = BarcodeSourceType::from((int) $mode); } try { $scan = $this->barcodeNormalizer->scanBarcodeContent($input, $modeEnum); } catch (InvalidArgumentException) { - // When the camera/barcode reader momentarily misreads a barcode whilst scanning - // return and empty result, so the good read data still remains visible - return new Response('', 200); + // Camera sometimes produces garbage decodes for a frame; ignore those. + return new JsonResponse(['ok' => false], 200); } + $decoded = $scan->getDecodedForInfoMode(); - $locale = $request->getLocale(); + // Resolve part (or null) $part = $this->barcodeParser->resolvePartOrNull($scan); - $found = $part !== null; + $redirectUrl = null; + if ($part !== null) { + // Redirector knows how to route parts, lots, and storelocations. + $redirectUrl = $this->barcodeParser->getRedirectURL($scan); + } + + // Build template vars $partName = null; $partUrl = null; $locations = []; $createUrl = null; - if ($found) { + if ($part !== null) { $partName = $part->getName(); - - // This is the same route BarcodeRedirector uses $partUrl = $this->generateUrl('app_part_show', ['id' => $part->getID()]); - - // Build locations (see below) $locations = $this->buildLocationsForPart($part); - } else { - // Reuse your centralized create-url logic (the helper you already extracted) $createUrl = $this->buildCreateUrlForScanResult($scan, $locale); } - return $this->render('label_system/scanner/augmented_result.html.twig', [ + // Render one fragment that shows: + // - decoded info (optional if you kept it) + // - part info + locations when found + // - create link when not found + $html = $this->renderView('label_system/scanner/augmented_result.html.twig', [ 'decoded' => $decoded, - 'found' => $found, + 'found' => ($part !== null), 'partName' => $partName, 'partUrl' => $partUrl, 'locations' => $locations, 'createUrl' => $createUrl, ]); + + return new JsonResponse([ + 'ok' => true, + 'found' => ($part !== null), + 'redirectUrl' => $redirectUrl, // client redirects only when infoMode=false + 'html' => $html, + 'infoMode' => $infoMode, + ], 200); } } diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index d0d609d3..0a67467f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -85,11 +85,6 @@ class ScanDialogType extends AbstractType 'required' => false, ]); - $builder->add('augmented_mode', CheckboxType::class, [ - 'label' => 'scan_dialog.augmented_mode', - 'required' => false, - ]); - $builder->add('submit', SubmitType::class, [ 'label' => 'scan_dialog.submit', ]); diff --git a/templates/label_system/scanner/augmented_result.html.twig b/templates/label_system/scanner/augmented_result.html.twig index 20eec82b..044d4ac6 100644 --- a/templates/label_system/scanner/augmented_result.html.twig +++ b/templates/label_system/scanner/augmented_result.html.twig @@ -54,10 +54,23 @@ {% endif %} {% else %}
- {% trans %}scan.qr_not_found{% endtrans %} + {% trans %}label_scanner.qr_part_no_found{% endtrans %}
{% endif %} + {# Decoded barcode fields #} + + + {% for key, value in decoded %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
+ + {# Whitespace under table and Input form fields #}

diff --git a/templates/label_system/scanner/scanner.html.twig b/templates/label_system/scanner/scanner.html.twig index ffac8234..ed657839 100644 --- a/templates/label_system/scanner/scanner.html.twig +++ b/templates/label_system/scanner/scanner.html.twig @@ -13,7 +13,7 @@
+ data-lookup-url="{{ path('scan_lookup') }}">
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 482d0c15..37b39085 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12039,12 +12039,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g Info mode (Decode barcode and show its contents, but do not redirect to part) - - - scan_dialog.augmented_mode - Augmented mode (Decode barcode, look up, and display database part information) - - label_scanner.decoded_info.title @@ -12063,6 +12057,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Part is not stored at any locations + + + label_scanner.qr_part_no_found + No part found for scanned barcode, click button above to Create Part + + label_generator.edit_profiles