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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user