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 %}
| {{ key }} | +{{ value }} |
+
|---|