From fe23899479d18e57f81352adf0ab60b6a0719a2a Mon Sep 17 00:00:00 2001
From: Scott Idem
Date: Fri, 20 Mar 2026 19:15:35 -0400
Subject: [PATCH] =?UTF-8?q?feat:=20leads=20QR=20UX=20=E2=80=94=20merged=20?=
=?UTF-8?q?confirm=20modes,=20faster=20scanning,=20correct=20capture=20ide?=
=?UTF-8?q?ntity?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Merge Rapid + Qualify scan modes into single Confirm mode with two-button card:
"Add & Scan Next" (resets) and "Add & View Lead" (navigates to detail). Same
two-button pattern on the reenable card: "Restore & Scan Next" / "Restore & View Lead".
Stale 'qualify' localStorage values normalized to 'rapid' via $derived.by().
- QR scanner speed: fps 10→25, qrbox 82%→88%, useBarCodeDetectorIfSupported (native
BarcodeDetector API on Chrome/Edge — significantly faster than ZXing JS fallback)
- Fix capture identity stored in external_person_id / group:
licensed exhibit user → their email; shared passcode → 'shared_passcode' label
(not the raw passcode); Aether user bypassing exhibit sign-in → access_type string
('trusted', 'manager', 'super', etc.). Consistent across all three lead capture
components (single scanner, multi scanner, manual search).
Co-Authored-By: Claude Sonnet 4.6
---
src/lib/elements/element_qr_scanner_v3.svelte | 11 +-
.../[badge_id]/ae_comp__badge_obj_view.svelte | 4 +-
.../ae_comp__lead_manual_search.svelte | 8 +-
.../ae_comp__lead_qr_scanner.svelte | 108 ++++++++++--------
.../ae_comp__lead_qr_scanner_multi.svelte | 7 +-
.../exhibit/[exhibit_id]/ae_tab__add.svelte | 22 ++--
6 files changed, 93 insertions(+), 67 deletions(-)
diff --git a/src/lib/elements/element_qr_scanner_v3.svelte b/src/lib/elements/element_qr_scanner_v3.svelte
index 7bcde1fb..688919b1 100644
--- a/src/lib/elements/element_qr_scanner_v3.svelte
+++ b/src/lib/elements/element_qr_scanner_v3.svelte
@@ -27,7 +27,7 @@
let {
start_qr_scanner = $bindable(true),
on_qr_scan_result,
- qr_fps = 10,
+ qr_fps = 25,
qr_facing_mode = 'environment'
}: Props = $props();
@@ -90,9 +90,14 @@
fps: qr_fps,
// Use a percentage of the viewfinder so it scales on any screen size
qrbox: (w: number, h: number) => {
- const side = Math.floor(Math.min(w, h) * 0.82);
+ const side = Math.floor(Math.min(w, h) * 0.88);
return { width: side, height: side };
- }
+ },
+ // Use native BarcodeDetector API on Chrome/Edge — significantly faster
+ // than the JS ZXing fallback used on Firefox/older Safari.
+ // Cast: experimentalFeatures exists at runtime but is missing from the
+ // html5-qrcode TypeScript type definitions for this version.
+ ...({ experimentalFeatures: { useBarCodeDetectorIfSupported: true } } as { experimentalFeatures: { useBarCodeDetectorIfSupported: boolean } })
},
on_scan_success,
on_scan_error
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
index b2fb381f..f51f2499 100644
--- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
+++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
@@ -186,7 +186,7 @@
if (layout === 'badge_3.5x5.5_pvc') {
// 3.5" × 5.5" PVC card — single-sided, compact
return {
- grp_name_title: '1.8in',
+ grp_name_title: '1.6in',
grp_name_title_flex: 'around',
name: '1.4in',
title: '0.55in',
@@ -446,7 +446,7 @@
m-0 p-0
px-1
overflow-clip
- flex flex-col gap-1
+ flex flex-col
items-stretch justify-between
"
>
diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_manual_search.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_manual_search.svelte
index f44185ff..82f470cf 100644
--- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_manual_search.svelte
+++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_manual_search.svelte
@@ -6,7 +6,7 @@
import { page } from '$app/state';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
- import { ae_api } from '$lib/stores/ae_stores';
+ import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { Eye, LoaderCircle, Search, ShieldOff, UserPlus } from '@lucide/svelte';
@@ -86,8 +86,10 @@
adding_id = badge_id;
add_error_id = '';
- // Use the actual signed-in licensed user's email (stored in auth_exhibit_kv)
- const user_email = $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.key || 'shared_passcode';
+ const kv = $events_loc.leads.auth_exhibit_kv?.[exhibit_id];
+ const user_email = kv?.type === 'licensed' && kv.key ? kv.key
+ : kv?.type === 'shared' ? 'shared_passcode'
+ : $ae_loc.access_type || 'anonymous';
try {
const result = await events_func.create_ae_obj__exhibit_tracking({
diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_qr_scanner.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_qr_scanner.svelte
index 4eaffbbe..9a755208 100644
--- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_qr_scanner.svelte
+++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_qr_scanner.svelte
@@ -16,7 +16,7 @@
import { events_func } from '$lib/ae_events/ae_events_functions';
import Element_qr_scanner_v3 from '$lib/elements/element_qr_scanner_v3.svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
- import { Camera, CircleAlert, CircleCheck, Eye, LoaderCircle, RefreshCw, RotateCcw, ShieldOff, UserPlus, X } from '@lucide/svelte';
+ import { Camera, CircleAlert, CircleCheck, Eye, LoaderCircle, RefreshCw, RotateCcw, ShieldOff, X } from '@lucide/svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { ae_EventBadge } from '$lib/types/ae_types';
@@ -102,7 +102,7 @@
}
}
- async function confirm_add_lead() {
+ async function confirm_add_lead(dest: 'scan_next' | 'view_lead' = 'scan_next') {
if (!found_badge || !found_badge.event_badge_id) {
console.warn('[leads] Guard failed — event_badge_id missing. found_badge:', found_badge);
return;
@@ -110,8 +110,14 @@
scanning_status = 'adding';
- // Use the actual signed-in licensed user's email
- const user_email = $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.key || 'shared_passcode';
+ // Resolve who is capturing this lead:
+ // licensed exhibit user → their email (kv.key)
+ // shared passcode → 'shared_passcode' label (don't store the actual passcode)
+ // Aether user (no kv) → access_type string ('trusted', 'manager', 'super', etc.)
+ const kv = $events_loc.leads.auth_exhibit_kv?.[exhibit_id];
+ const user_email = kv?.type === 'licensed' && kv.key ? kv.key
+ : kv?.type === 'shared' ? 'shared_passcode'
+ : $ae_loc.access_type || 'anonymous';
try {
const result = await events_func.create_ae_obj__exhibit_tracking({
@@ -128,11 +134,11 @@
scanning_status = 'success';
if (on_lead_added) on_lead_added(found_badge);
- if (scan_qualify === 'qualify' && new_tracking_id) {
- // Qualify mode: navigate directly to lead detail to fill in notes/qualifiers
+ if (dest === 'view_lead' && new_tracking_id) {
+ // View Lead: navigate directly to lead detail to fill in notes/qualifiers
goto(`/events/${page.params.event_id}/leads/exhibit/${exhibit_id}/lead/${new_tracking_id}`);
} else {
- // Rapid/auto mode: auto-reset after 2 seconds to scan the next person
+ // Scan Next / auto mode: auto-reset after 2 seconds to scan the next person
setTimeout(reset_scanner, 2000);
}
} else {
@@ -167,7 +173,7 @@
}
}
- async function confirm_reenable_lead() {
+ async function confirm_reenable_lead(dest: 'scan_next' | 'view_lead' = 'scan_next') {
// Re-activate a lead that was previously removed (enable=false).
// existing_tracking_id is already set from the map or the API fallback search.
if (!existing_tracking_id) return;
@@ -185,9 +191,11 @@
new_tracking_id = existing_tracking_id;
scanning_status = 'success';
if (on_lead_added && found_badge) on_lead_added(found_badge);
- // Re-enabled lead: success card shows "View Details" link — user navigates manually.
- // Auto-reset after 2s so the scanner is ready for the next badge.
- setTimeout(reset_scanner, 2000);
+ if (dest === 'view_lead' && new_tracking_id) {
+ goto(`/events/${page.params.event_id}/leads/exhibit/${exhibit_id}/lead/${new_tracking_id}`);
+ } else {
+ setTimeout(reset_scanner, 2000);
+ }
} else {
scanning_status = 'error';
error_msg = 'Failed to restore lead. Please try again.';
@@ -247,23 +255,25 @@
This lead was removed. Re-activate to restore their record including any saved notes and responses.
-
-
-
-
- View Existing Record
-
+
+
+
+
+
- {#if scan_qualify === 'auto'}
-
+ {#if scan_qualify === 'auto' || scanning_status === 'adding'}
+
- Auto-adding...
+ {scan_qualify === 'auto' ? 'Auto-adding...' : 'Adding Lead...'}
{:else}
-
+
+
+
+
+