Skip to content

Scan to Item — Frontend and UX

The frontend gives operators one place to scan Arda card QRs and product barcodes, then routes each result to the next useful action. The current prototype shares the same behavior across mobile cameras, desktop cameras, and keyboard-wedge scanners while using layouts suited to each screen size.

StatusWorking prototype on feature/pdev-1161-scan-to-item
Reference frontendTypeScript and React in arda-frontend-app
Enrichment runtimeIn-process engine in the arda-frontend-app BFF
Operations dataExisting Item, ItemSupply, Kanban Card, and order queue flows
Local demoMock Operations data with fixture or in-process engine enrichment

The first production version needs to:

  • accept Arda QRs and supported product barcodes without asking the user to choose a scan mode;
  • apply the same payload classification and routing rules on mobile and desktop;
  • keep Arda card identifiers out of product enrichment;
  • show an existing Item or Card instead of creating a duplicate;
  • open a reviewable, editable Item form when enrichment finds a product;
  • preserve price currency, package units, supplier information, images, and the scanned identifier when available;
  • require a human check when identity or quantity evidence is uncertain;
  • support continuous scanning and batch actions without repeatedly closing the camera;
  • recover from camera, lookup, image, create, and bind failures without losing the user’s work; and
  • keep QR binding and Item creation explicit, including the case where Item creation succeeds but binding fails; and
  • offer printing the newly created Kanban Card after a standalone Item is published.

For the prototype, a missing result is preferable to a confident false match. The user can always continue with manual entry.

The header Scan button, the Items page Scan new items action, /scan, and card scan deep links all open the shared scan workflow. The Item sidebar offers a single product lookup bar that takes a barcode, product URL, or typed description, with an inline camera for barcodes. Card binding starts from the scan-first flow or is offered after a standalone Item is published.

The scanner accepts three input methods:

InputMobileDesktop
CameraYesYes
Keyboard-wedge scannerNoYes
Typed or pasted product queryItem formItem form

Desktop uses the application’s shared right-hand Sheet as a persistent, non-modal scan workspace. A 16:9 camera preview sits at the top. The selected Card and its quick actions appear on one surface below it, while batch selection and bulk actions stay on a separate surface. This keeps the Items page visible and avoids stacking a scan modal with the Item sidebar.

Selecting a recent scan restores its quick-action card first. It does not open the Item sidebar until the user chooses View/Edit. Each compact batch row can be selected independently, opened for quick actions, or removed with its hover/focus action. Bulk actions use neutral buttons so they do not compete with the selected Card’s primary action. Review batch exposes status filters, text search, select all/deselect, order queue, restock, state changes, edit, and removal.

Mobile keeps the camera full-screen and puts the latest result and batch affordance near the bottom edge. The same scan state and routing code is used on desktop, but the mobile layout favors one-handed actions and larger touch targets. Swipe actions remain available for common row actions, with buttons as the accessible fallback.

Classification happens once, before lookup. A product barcode scanned after an Arda QR still follows the product path; this lets an operator recover naturally from pointing the camera at the wrong code.

PayloadExamplesResult
Item deep link/item/{itemId}Open the Item
Bound Arda card/qr/{cardId}, arda://card/{cardId}, legacy Kanban Card URL, or bare UUIDAdd the Card to the batch and show quick actions
Unbound Arda cardA generated but unused /qr/{cardId}Open Set up this card
Product barcodeEAN-13, EAN-8, UPC-A, UPC-E, Code 128, Code 93, Code 39, Codabar, ITF, or Data MatrixFind the Item or start enrichment
Product URL or textEntered in the Item formStart enrichment
Unsupported valueAn unrecognized camera or scanner resultExplain the problem and keep scanning

PlantUML diagram

The first accepted decode owns the interaction until its lookup completes. Repeated camera detections are ignored during that interval, and scanning a Card already in the batch promotes its existing result instead of adding a duplicate row.

Every payload that reaches Start enrichment resolves through one call: the frontend sends the raw barcode, URL, or text to POST /api/enrichment/import and lets the BFF classify it server-side. There is no client-side Amazon detection or separate Amazon-only request; an Amazon URL, a non-Amazon URL, a barcode, and free text all go through the same endpoint.

A product barcode with no Item match opens the Item sidebar and begins enrichment immediately. On desktop, the scan workspace is parked rather than discarded, so its QR batch returns when the user closes or completes the Item form. The form uses a single lookup bar for barcode, URL, or free-text input; every input goes to the same /api/enrichment/import call rather than a separate Amazon-only lookup. The barcode camera expands inline above the lookup bar; there is no separate floating desktop camera. Results render as an inline buying-options carousel, with an alternate-image picker below the selected card, reusing the product-match UI’s larger images, supplier offers, verdict labels, and price currency.

Choosing a result copies suggested values into the normal Item form. Suggested sections are highlighted, and Review next moves through them in form order. Users can edit or clear the name, image, price, currency, supplier, SKU, URL, minimum quantity, order quantity, and unit labels before publishing.

The suggested supplier is free text from the provider, so it is auto-linked to an existing tenant vendor only on an unambiguous normalized match — punctuation and case differences link (“McMaster Carr” → “McMaster-Carr”), a single containment links (“Grainger” → “W.W. Grainger”), and anything ambiguous stays unlinked for the user to pick. Durable supplier aliases are the tenant-memory work described in the backend design.

The form treats package size and ordering quantity separately. A listing such as “case of 24” can suggest pack of 24 as the unit, but minimum and order amounts stay at one unless the service returns verified quantity evidence. Prices keep the selected offer’s supported ISO currency instead of defaulting every result to USD.

The quick create-and-bind form uses the same rules. It uploads a captured image before showing the preview, normalizes known thumbnail URLs when a source image is available, and leaves a neutral placeholder when a remote image cannot load. If enrichment returns no match or is unavailable, the scanned input stays visible and manual entry remains usable.

Closing or clearing the Item sidebar resets its lookup, results, suggested-field markers, pending bind state, and form values. Publishing a standalone Item offers three explicit next steps: bind a pre-printed Card, print the newly created Card, or finish without one. Printing refreshes the Item’s Cards before sending the new Card to the existing print viewer; if the Card is not ready or printing fails, the decision stays open for retry.

An unbound Arda QR opens Set up this card with two paths:

  1. Create new item opens the Item sidebar with the scanned QR retained, then creates the Item and binds that Card.
  2. Attach to existing item shows recently added Items immediately and also supports search. This path alone offers Bind and add to order queue.

On desktop, these choices replace the selected-result surface inside the scan sidebar. The camera feed remains visible while the first choice is shown, but decoding pauses so the same blank QR is not handled again. Clear dismisses the blank-card result and resumes decoding. Moving into existing-Item search collapses the camera and hides the batch until the user returns; the batch itself is preserved. Creating a new Item parks the scan sidebar and shows the QR being bound in the Item sidebar before publish.

PlantUML diagram

Create and bind remain two operations in the prototype. The UI records the created Item before starting the bind, so a failed bind can be retried without creating a duplicate Item. A conflict reports that the code is already bound.

After a standalone Item is published, the sidebar offers Bind a printed card against the newly created Item. The camera opens inside that panel, and the Manage Cards panel provides the same inline recovery path later. The empty create form does not expose binding; operators who already have a card should begin with the shared scanner so its QR context follows Item creation.

The scanner uses focused libraries instead of a second desktop implementation:

  • qr-scanner decodes Arda QR codes;
  • the barcode-detector ponyfill decodes the supported product barcode formats and loads its ZXing WASM asset from the app’s static assets; and
  • one React camera hook owns camera startup, decode dispatch, retry, and shutdown for both presentations.

The camera hook can pause decode callbacks without tearing down the media stream. Desktop uses that state while showing blank-card quick actions, which avoids a black or stopped preview without allowing duplicate reads.

Camera errors distinguish missing permission, unavailable hardware, and camera contention when the browser exposes enough information. The UI offers retry and manual product entry. Chrome is the best-tested browser for the prototype; Safari uses the same decode path and permission guidance, but physical-device Safari coverage is still required before production.

Many desktops have no webcam, so a USB or Bluetooth barcode scanner in keyboard-wedge mode is a first-class input, not a fallback. On desktop the workspace listens for wedge keystrokes at the window level and dispatches on the scanner’s trailing Enter, so the operator does not have to focus a field first; the listener steps aside whenever a text input is focused, so typing a query still works. A wedge scan is classified like any other decode: an 8–14 digit code routes to product lookup, anything else to Arda QR resolve. Because this path never touches the camera, a machine with no webcam can still scan, and repeated wedge scans stack into the same batch — the same continuous-scan behavior the camera path uses.

A Bluetooth wedge is invisible to the browser — it registers as a keyboard, with no API to tell whether one is attached — so no-camera is treated as a normal ready state for wedge-first users, not a failure. When the camera cannot start (no webcam, or one that is blocked or busy), the desktop workspace shows a “Ready for your barcode scanner” state: it confirms a USB or Bluetooth scanner works without a camera and offers a “try a webcam” link. Underneath, qr-scanner reports a missing, blocked, or busy camera the same way, so the workspace does not try to distinguish those cases in the copy.

The unified ScanWorkspace ships dark behind NEXT_PUBLIC_SCAN_V2. With the flag off, desktop keeps the previous DesktopScanView; with it on, desktop and mobile both use the new workspace. This lets the scan-and-enrich flow merge and deploy without changing the default desktop experience until the flag is flipped, so the new scanner can be exercised in a real environment before it becomes the default.

The July workflow review produced the following changes.

FeedbackCurrent result
Standalone Add Item did not offer a next Card stepPublish now offers binding a printed Card, printing the new Card, or finishing without one; the sidebar and Manage Cards panel can also scan a printed Card later
Created Item briefly showed another ItemCreate and bind state is isolated to the active Item, and bind retry retains that Item ID
Scan next card left the workflowThe camera stays open for continuous scanning
Scanning an Arda QR in product lookup got stuckCard codes are rejected with guidance in product fields and routed correctly by the shared scanner
Product images were too small or blurryResults use larger previews, support zoom, and normalize known source-image URLs
Captured photo did not renderThe uploaded URL is applied to the form immediately and covered by a regression test
Units could not be edited during creationMinimum and order quantity units are editable in both Item forms
Suggested bulk quantities were unpredictableUnverified quantities stay at one and show an explanation; pack labels remain editable
Enrichment had no loading feedbackLookup progress and loading indicators are visible and announced
Imported fields were hard to clearPrice, supplier, image, lookup state, and the complete form have clear actions
Batch rows were not directly usefulRows are selectable and expose queue, restock, and view actions
Scan close control was too smallScanner close controls use at least a 44-pixel target
Existing Items were hard to find while bindingRecently added Items appear before search
Order queue edit was unresponsiveThe active grouped and ungrouped queue views route Edit Item to the Item form; this remains covered by queue tests
Price currency was lostOffer currency is displayed and preserved in the Item payload
Batch and single-result actions competedDesktop separates them into two surfaces and keeps bulk actions neutral; orange is reserved for the selected Card’s primary action

The prototype also includes pack-size suggestions, multiple product barcodes on quick-created Items, swipe actions, Review next, and optional bind-and-queue for an existing Item. Multiple identifiers are currently recorded in prototype Item data; a production identifier model is future work.

ConditionBehavior
Camera permission denied or unavailableExplain the likely cause, offer retry, and keep manual product entry available
Unsupported scanShow concise guidance, release the processing lock, and resume the camera
Card lookup failsKeep the workspace open and allow another scan
Enrichment returns no matchPreserve the query and leave the manual form usable
Enrichment service is unavailableShow a service error without clearing user input
Bind conflictsExplain that the QR is already bound
Item created but bind failsConfirm Item creation and retry only the bind
New Card is not ready or print failsKeep the post-publish choices open and allow retry or another path
Image cannot loadShow a placeholder without blocking publish

Buttons and scanner controls have accessible names, visible keyboard focus, and touch-sized targets. Async messages use status regions, destructive repeat actions are disabled while requests run, and motion respects prefers-reduced-motion. Before production, the remaining accessibility work is a full focus-order, focus-trap, screen-reader, and zoom audit across both presentations.

Start the frontend with mock Operations data and deterministic enrichment fixtures:

Terminal window
cd arda-frontend-app
npm run dev:mock

The fixture handler returns known examples plus a generic reviewable result for other inputs. That is the default and needs no keys.

To try real enrichment in the UI, keep Operations mocked but let a scan reach the in-process engine, and add provider keys to .env.local:

NEXT_PUBLIC_MOCK_MODE=true
NEXT_PUBLIC_ENRICHMENT_MODE=service
NEXT_PUBLIC_SCAN_V2=true
BARCODE_LOOKUP_API_KEY=...
# optional
ENRICHMENT_EXA_ENABLED=true
EXA_API_KEY=...

With NEXT_PUBLIC_ENRICHMENT_MODE=service, MSW steps aside and a scan hits the BFF route POST /api/enrichment/import, which runs the engine in-process against those providers — there is no separate service or :8080 proxy anymore. NEXT_PUBLIC_SCAN_V2=true opts the desktop into the new unified workspace. Restart the frontend after changing modes.

What is and is not real in this mode: enrichment is real (live product data, images, offers). Operations, item save, and QR resolve/bind stay mocked — those backend routes do not exist yet, so dev:mock serves the contract from bind-qr.openapi.yaml through MSW. Full end-to-end against real Operations and real bind routes waits on the backend building them. To exercise the engine against the live corpus outside the app, run npm run eval:live (keys from the environment; ENRICHMENT_FIXTURES_ENABLED=true runs it offline).

ScanWorkspace.tsx coordinates the workflow. MobileScanView.tsx is the entry switch that selects the V2 workspace or the previous DesktopScanView by the NEXT_PUBLIC_SCAN_V2 flag. Product lookup, Item mapping, batch mutations, camera behavior, and responsive presentation live in focused modules. The previous DesktopScanView remains as the flag-off fallback; the modal-era layout is retired. Quick Item creation is also split by responsibility so enrichment orchestration, lookup and camera controls, offers, editable fields, and payload construction can be reviewed independently.

AreaMain implementation
Shared scan state and routingsrc/components/scan/ScanWorkspace.tsx, MobileScanView.tsx, productBarcodeLookup.ts
Scan Item mapping and batch mutationssrc/components/scan/scanItemMappers.ts, useScanBatchActions.tsx
Camera lifecycle and decodingsrc/components/scan/useCameraScanner.ts
Desktop Sheet and layoutsrc/components/scan/DesktopScanSidebar.tsx, DesktopScanWorkspace.tsx
Desktop camera, batch, recents, and result UIsrc/components/scan/DesktopScanCamera.tsx, DesktopBatchActions.tsx, DesktopRecentScans.tsx, LatestScanCard.tsx
Mobile camera and shared rowssrc/components/scan/MobileScanCamera.tsx, ScannedCardsPane.tsx, SwipeableScanRow.tsx
QR setupsrc/components/scan/BindQrCodeSheet.tsx
Quick create and bindsrc/components/scan/QuickItemCreate.tsx, QuickItemLookup.tsx, QuickItemOffers.tsx, QuickItemFields.tsx, quickItemCreateModel.ts
Full Item form enrichmentsrc/components/items/ItemFormPanel.tsx, useItemEnrichmentPanel.ts, EnrichmentLookupBar.tsx, EnrichmentSearchResults.tsx
Enrichment contract adaptersrc/lib/enrichmentClient.ts
Mock/service switchsrc/mocks/handlers/enrichment.ts, tools/dev.ts

Unit and integration tests cover payload classification, QR/product separation, duplicate suppression, camera errors, quick actions, batch actions, enrichment mapping, suggestion review, safe quantities, image upload, currency preservation, create-only, create-and-bind, partial bind failure, conflicts, post-publish binding and printing choices, and Item sidebar reset behavior.

End-to-end coverage exercises the shared desktop workflow, keyboard-wedge input, deep links, mobile presentation, and Item details from scan context.

Before a pilot, the scan workflow must pass a short physical-device matrix covering supported product barcodes and Arda QRs in Chrome and Safari. The same pass must verify first-run and previously-denied camera permissions, camera contention, focus order, focus trapping, screen-reader announcements, and browser zoom on both presentations.

  • Replace the browser-side scan through up to 100 Item notes and SKUs with a tenant-scoped backend query over structured product identifiers.
  • Add production QR resolve and bind routes. Blank UUIDs are generated locally and do not require an issuance API or server-side allocation state.
  • Route https://arda.cards/qr/{cardId} to the authenticated app landing flow before printing cards for use outside the local demo.
  • Keep the blank Card generator in developer tooling rather than the product frontend.
  • Derive tenant and author context in the authenticated BFF instead of using the local demo tenant.
  • Decide whether multiple UPC, EAN, GTIN, and supplier identifiers belong on Item, ItemSupply, or a separate identifier resource.
  • Add scan and enrichment telemetry without recording raw provider payloads or user-entered free text.
  • Decide whether create-and-bind should remain a retryable two-step UI operation or become one backend command.
  1. When an Item exists without a Card, should a product scan open Item details, start card binding, or offer both?
  2. Which roles may generate blank QR codes and bind them to Items?
  3. Which enrichment evidence should appear inline, and what should move to a details view?
  4. Should users choose image candidates independently from supplier offers?