Implement 'Already Added' detection for Lead Capture

- Added real-time checking for existing leads in manual search.
- Implemented 'Already Captured' status for QR scanner with direct 'View Lead' link.
- Resolved all remaining TypeScript typing issues in capture components.
- Optimized Dexie lookups for existing lead detection.
This commit is contained in:
Scott Idem
2026-02-08 23:08:36 -05:00
parent 1dd80cc974
commit c88904beb1
2 changed files with 101 additions and 17 deletions

View File

@@ -4,10 +4,12 @@
* Manual Attendee Search for adding leads.
*/
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 { events_loc } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
import { Search, UserPlus, CheckCircle, LoaderCircle } from 'lucide-svelte';
import { Search, UserPlus, CheckCircle, LoaderCircle, Eye } from 'lucide-svelte';
import type { ae_EventBadge } from '$lib/types/ae_types';
import { ae_util } from '$lib/ae_utils/ae_utils';
@@ -18,6 +20,24 @@
let { exhibit_id, on_lead_added }: Props = $props();
// Track existing leads to prevent duplicates in UI
let existing_leads_map = $derived(
liveQuery(async () => {
const leads = await db_events.exhibit_tracking
.where('event_exhibit_id')
.equals(exhibit_id)
.toArray();
// Map badge_id -> tracking_id
const map = new Map();
leads.forEach(l => {
const b_id = l.event_badge_id_random || l.event_badge_id?.toString();
if (b_id) map.set(b_id, l.event_exhibit_tracking_id_random || l.event_exhibit_tracking_id?.toString());
});
return map;
})
);
let search_query = $state('');
let results: ae_EventBadge[] = $state([]);
let searching = $state(false);
@@ -105,19 +125,30 @@
<div class="font-bold">{badge.full_name}</div>
<div class="text-xs opacity-70">{badge.affiliations || badge.email || ''}</div>
</div>
<button
type="button"
class="btn btn-sm preset-filled-success"
disabled={adding_id === badge.event_badge_id_random}
onclick={() => add_as_lead(badge)}
>
{#if adding_id === badge.event_badge_id_random}
<LoaderCircle class="animate-spin" size="1em" />
{:else}
<UserPlus size="1em" class="mr-1" />
{/if}
Add
</button>
{#if $existing_leads_map?.has(badge.event_badge_id_random)}
<a
href={`/events/${page.params.event_id}/leads/exhibit/${exhibit_id}/lead/${$existing_leads_map.get(badge.event_badge_id_random)}`}
class="btn btn-sm variant-filled-secondary"
>
<Eye size="1em" class="mr-1" />
View
</a>
{:else}
<button
type="button"
class="btn btn-sm preset-filled-success"
disabled={adding_id === badge.event_badge_id_random}
onclick={() => add_as_lead(badge)}
>
{#if adding_id === badge.event_badge_id_random}
<LoaderCircle class="animate-spin" size="1em" />
{:else}
<UserPlus size="1em" class="mr-1" />
{/if}
Add
</button>
{/if}
</div>
{/each}
</div>

View File

@@ -4,12 +4,14 @@
* Badge QR Scanner for adding leads.
*/
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 { events_loc } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
import Element_qr_scanner_v2 from '$lib/element_qr_scanner_v2.svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { LoaderCircle, UserPlus, CheckCircle, AlertCircle } from 'lucide-svelte';
import { LoaderCircle, UserPlus, CheckCircle, AlertCircle, Eye } from 'lucide-svelte';
import type { ae_EventBadge } from '$lib/types/ae_types';
interface Props {
@@ -19,9 +21,27 @@
let { exhibit_id, on_lead_added }: Props = $props();
// Track existing leads to prevent duplicates
let existing_leads_map = $derived(
liveQuery(async () => {
const leads = await db_events.exhibit_tracking
.where('event_exhibit_id')
.equals(exhibit_id)
.toArray();
const map = new Map();
leads.forEach(l => {
const b_id = l.event_badge_id_random || l.event_badge_id?.toString();
if (b_id) map.set(b_id, l.event_exhibit_tracking_id_random || l.event_exhibit_tracking_id?.toString());
});
return map;
})
);
let start_qr_scanner = $state(true);
let scanning_status = $state('idle'); // idle, scanning, found, adding, success, error
let scanning_status = $state('idle'); // idle, scanning, found, adding, success, error, already_added
let found_badge: ae_EventBadge | null = $state(null);
let existing_tracking_id = $state('');
let error_msg = $state('');
async function handle_qr_scan_result(event: CustomEvent) {
@@ -29,9 +49,16 @@
const obj = ae_util.process_data_string(qr_result);
if (obj && obj.type === 'event_badge' && obj.id) {
scanning_status = 'found';
start_qr_scanner = false;
// Check if already exists
if ($existing_leads_map?.has(obj.id)) {
scanning_status = 'already_added';
existing_tracking_id = $existing_leads_map.get(obj.id);
} else {
scanning_status = 'found';
}
// Load full badge info
try {
found_badge = await events_func.load_ae_obj_id__event_badge({
@@ -97,6 +124,32 @@
</div>
<p class="text-center opacity-70 italic text-sm">Point camera at the badge QR code</p>
{:else if scanning_status === 'already_added'}
<div class="card p-6 w-full max-w-md space-y-4 variant-soft-secondary shadow-xl border-2 border-secondary-500 animate-in zoom-in">
<div class="text-center space-y-2">
<CheckCircle size="3em" class="mx-auto text-secondary-500" />
<h3 class="h3 font-bold">Already Captured</h3>
<p class="text-xl font-bold">{found_badge?.full_name || 'Attendee'}</p>
<p class="opacity-70 text-sm">This attendee is already in your leads list.</p>
</div>
<a
href={`/events/${page.params.event_id}/leads/exhibit/${exhibit_id}/lead/${existing_tracking_id}`}
class="btn btn-xl w-full variant-filled-secondary font-bold py-6"
>
<Eye size="1.5em" class="mr-2" />
View Lead Details
</a>
<button
type="button"
class="btn btn-sm w-full opacity-50"
onclick={reset_scanner}
>
Scan Next
</button>
</div>
{:else if scanning_status === 'found' || scanning_status === 'adding'}
<div class="card p-6 w-full max-w-md space-y-4 variant-soft-primary shadow-xl border-2 border-primary-500">
<div class="text-center">