Fix Lead List filtering and resolve Svelte 5 subscription crash

- Refactored Lead List to use direct observable subscription for better reactivity.
- Implemented in-memory 'Hard Guard' filter to ensure licensee selection is strictly enforced.
- Fixed 'TypeError: s.subscribe is not a function' by removing legacy $ prefix from resolved props.
- Resolved TypeScript typing errors in Lead Detail and Search components.
- Migrated Badge Search icons to Lucide.
This commit is contained in:
Scott Idem
2026-02-08 23:03:35 -05:00
parent 8787f8c2ff
commit 111ef76d14
5 changed files with 63 additions and 29 deletions

View File

@@ -442,8 +442,8 @@ export async function search__exhibit_tracking({
log_lvl = 0 log_lvl = 0
}: { }: {
api_cfg: any; api_cfg: any;
event_id: string; event_id: string | undefined;
event_exhibit_id: string; event_exhibit_id: string | undefined;
fulltext_search_qry_str?: string | null; fulltext_search_qry_str?: string | null;
qry_group?: string | null; qry_group?: string | null;
qry_external_person_id?: string | null; qry_external_person_id?: string | null;
@@ -460,6 +460,8 @@ export async function search__exhibit_tracking({
console.log(`*** search__exhibit_tracking() *** exhibit_id=${event_exhibit_id} ft=${fulltext_search_qry_str}`); console.log(`*** search__exhibit_tracking() *** exhibit_id=${event_exhibit_id} ft=${fulltext_search_qry_str}`);
} }
if (!event_id || !event_exhibit_id) return [];
const search_query: any = { const search_query: any = {
q: fulltext_search_qry_str || '', q: fulltext_search_qry_str || '',
and: [ and: [

View File

@@ -171,9 +171,9 @@
class="btn btn-lg preset-tonal-success border border-success-500 hover:preset-tonal-success text-2xl font-bold w-48 transition-all" class="btn btn-lg preset-tonal-success border border-success-500 hover:preset-tonal-success text-2xl font-bold w-48 transition-all"
> >
{#if $events_sess.badges.search_status === 'loading'} {#if $events_sess.badges.search_status === 'loading'}
<span class="fas fa-spinner fa-spin mx-1"></span> <LoaderCircle class="animate-spin mx-1" />
{:else} {:else}
<span class="fas fa-search mx-1"></span> <Search class="mx-1" />
{/if} {/if}
Search Search
</button> </button>
@@ -222,7 +222,7 @@
}} }}
class="btn btn-sm preset-tonal-primary border border-primary-500" class="btn btn-sm preset-tonal-primary border border-primary-500"
> >
<span class="fas fa-qrcode mr-1"></span> <QrCode size="1em" class="mr-1" />
QR Scan QR Scan
</button> </button>
{:else} {:else}
@@ -235,7 +235,7 @@
}} }}
class="btn btn-sm preset-tonal-primary border border-primary-500" class="btn btn-sm preset-tonal-primary border border-primary-500"
> >
<span class="fas fa-search mr-1"></span> <Search size="1em" class="mr-1" />
Search Search
</button> </button>
{/if} {/if}
@@ -250,9 +250,9 @@
title="Toggle using the ID list or not." title="Toggle using the ID list or not."
> >
{#if $events_loc.badges.use_id_li} {#if $events_loc.badges.use_id_li}
<span class="fas fa-toggle-on text-green-600 mr-1"></span> <Check size="1.2em" class="text-green-600 mr-1" />
{:else} {:else}
<span class="fas fa-toggle-off text-red-600 mr-1"></span> <X size="1.2em" class="text-red-600 mr-1" />
{/if} {/if}
Use ID List Use ID List
</button> </button>

View File

@@ -71,23 +71,43 @@
$events_loc.leads.tab[exhibit_id] = new_tab; $events_loc.leads.tab[exhibit_id] = new_tab;
} }
let tracking_id_li: Array<string> = $state([]); let search_debounce_timer: any = null; let tracking_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null;
let last_search_id = 0; let last_search_id = 0;
let last_executed_key = ''; let last_executed_key = '';
let log_lvl = 1; let log_lvl = 1;
// Stable LiveQuery Pattern // --- NEW: Direct Reactive List Pattern ---
let lq__event_exhibit_tracking_obj_li = $derived.by(() => { let raw_lead_li: any[] = $state([]);
// Final filtered list that the UI actually sees
// Applying the HARD GUARD here ensures that no matter where the data came from
// (API or IDB), it MUST match the selected licensee.
let filtered_lead_li = $derived.by(() => {
const licensee_filter = search_params.licensee_email;
if (licensee_filter === 'all') return raw_lead_li;
return raw_lead_li.filter(lead => {
const capturer = lead.external_person_id || lead.group;
return capturer === licensee_filter;
});
});
// Subscribe to the Lead List
$effect(() => {
const ids = tracking_id_li; const ids = tracking_id_li;
const exhibit_id = page.params.exhibit_id; const exhibit_id = page.params.exhibit_id;
const has_search = !!$events_loc.leads.tracking__qry__search_text;
return liveQuery(async () => { const observable = liveQuery(async () => {
// 1. Specific IDs provided (from API Search or Manual Entry)
if (Array.isArray(ids) && ids.length > 0) { if (Array.isArray(ids) && ids.length > 0) {
const results = await db_events.exhibit_tracking.bulkGet(ids); const results = await db_events.exhibit_tracking.bulkGet(ids);
return results.filter((item) => item !== undefined); return results.filter((item) => item !== undefined);
} }
if (exhibit_id && !$events_loc.leads.tracking__qry__search_text) { // 2. Fallback broad search (Initial load or no search text)
if (exhibit_id && !has_search) {
return await db_events.exhibit_tracking return await db_events.exhibit_tracking
.where('event_exhibit_id') .where('event_exhibit_id')
.equals(exhibit_id) .equals(exhibit_id)
@@ -97,10 +117,16 @@
return []; return [];
}); });
const subscription = observable.subscribe(res => {
raw_lead_li = res;
});
return () => subscription.unsubscribe();
}); });
// Exhibit Info // Exhibit Info
let lq__exhibit_obj = liveQuery(() => { const lq__exhibit_obj = liveQuery(() => {
const exhibit_id = page.params.exhibit_id; const exhibit_id = page.params.exhibit_id;
if (!exhibit_id) return undefined; if (!exhibit_id) return undefined;
return db_events.exhibit.get(exhibit_id); return db_events.exhibit.get(exhibit_id);
@@ -183,16 +209,18 @@
// 1. FAST PATH: Local IDB Search // 1. FAST PATH: Local IDB Search
if (!remote_first) { if (!remote_first) {
try { try {
const target_exhibit_id = exhibit_id;
const target_licensee_email = params.licensee_email;
let local_results = await db_events.exhibit_tracking let local_results = await db_events.exhibit_tracking
.where('event_exhibit_id') .where('event_exhibit_id')
.equals(exhibit_id) .equals(target_exhibit_id)
.filter((tracking) => { .filter((tracking) => {
// 1. Licensee Email Filter // 1. Licensee Email Filter
if (params.licensee_email !== 'all') { if (target_licensee_email !== 'all') {
if (tracking.external_person_id !== params.licensee_email) return false; if (tracking.external_person_id !== target_licensee_email) return false;
} }
// 2. Text Search Filter
if (qry_str) { if (qry_str) {
const name = ( const name = (
tracking.event_badge_full_name ?? '' tracking.event_badge_full_name ?? ''
@@ -284,12 +312,16 @@
order_by_li = { created_on: 'DESC' }; order_by_li = { created_on: 'DESC' };
} }
const q_event_id: string = page.params.event_id ?? '';
const q_exhibit_id: string = exhibit_id ?? '';
const q_licensee_email: string | null = (params.licensee_email !== 'all') ? (params.licensee_email ?? '') : null;
const results = await events_func.search__exhibit_tracking({ const results = await events_func.search__exhibit_tracking({
api_cfg: $ae_api, api_cfg: $ae_api,
event_id: page.params.event_id || '', event_id: q_event_id,
event_exhibit_id: exhibit_id, event_exhibit_id: q_exhibit_id,
fulltext_search_qry_str: qry_str || null, fulltext_search_qry_str: qry_str || null,
qry_external_person_id: params.licensee_email !== 'all' ? params.licensee_email : null, qry_external_person_id: q_licensee_email,
order_by_li, order_by_li,
limit: 150 limit: 150
}); });
@@ -463,7 +495,7 @@
<p class="text-xl">Searching leads...</p> <p class="text-xl">Searching leads...</p>
</div> </div>
{:else} {:else}
<Comp_exhibit_tracking_obj_li {lq__event_exhibit_tracking_obj_li} /> <Comp_exhibit_tracking_obj_li lq__event_exhibit_tracking_obj_li={filtered_lead_li} />
{/if} {/if}
</div> </div>
{:else if active_tab === 'manage'} {:else if active_tab === 'manage'}

View File

@@ -25,11 +25,11 @@
</script> </script>
<div class="ae_comp__exhibit_tracking_obj_li w-full px-2 sm:px-4"> <div class="ae_comp__exhibit_tracking_obj_li w-full px-2 sm:px-4">
{#if !$lq__event_exhibit_tracking_obj_li} {#if !lq__event_exhibit_tracking_obj_li}
<div class="flex justify-center p-10"> <div class="flex justify-center p-10">
<span class="fas fa-spinner fa-spin fa-2x opacity-20"></span> <span class="fas fa-spinner fa-spin fa-2x opacity-20"></span>
</div> </div>
{:else if $lq__event_exhibit_tracking_obj_li.length === 0} {:else if lq__event_exhibit_tracking_obj_li.length === 0}
<div class="card p-8 text-center variant-soft-surface"> <div class="card p-8 text-center variant-soft-surface">
<p class="text-xl opacity-50">No leads found yet.</p> <p class="text-xl opacity-50">No leads found yet.</p>
<p class="text-sm opacity-50 mt-2"> <p class="text-sm opacity-50 mt-2">
@@ -40,12 +40,12 @@
<div class="space-y-4"> <div class="space-y-4">
<div class="flex justify-between items-center px-2"> <div class="flex justify-between items-center px-2">
<span class="text-sm font-semibold opacity-50"> <span class="text-sm font-semibold opacity-50">
{$lq__event_exhibit_tracking_obj_li.length} Leads Collected {lq__event_exhibit_tracking_obj_li.length} Leads Collected
</span> </span>
</div> </div>
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
{#each $lq__event_exhibit_tracking_obj_li as event_tracking_obj} {#each lq__event_exhibit_tracking_obj_li as event_tracking_obj}
<a <a
href={`/events/${page.params.event_id}/leads/exhibit/${event_tracking_obj.event_exhibit_id}/lead/${event_tracking_obj.event_exhibit_tracking_id}`} href={`/events/${page.params.event_id}/leads/exhibit/${event_tracking_obj.event_exhibit_id}/lead/${event_tracking_obj.event_exhibit_tracking_id}`}
class="card card-hover p-4 variant-filled-surface border-l-4 border-primary-500 flex flex-col md:flex-row gap-4 items-start md:items-center" class="card card-hover p-4 variant-filled-surface border-l-4 border-primary-500 flex flex-col md:flex-row gap-4 items-start md:items-center"

View File

@@ -179,7 +179,7 @@
<Element_ae_crud_v2 <Element_ae_crud_v2
api_cfg={$ae_api} api_cfg={$ae_api}
object_type="event_exhibit_tracking" object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id} object_id={exhibit_tracking_id ?? ''}
field_name="exhibitor_notes" field_name="exhibitor_notes"
field_type="textarea" field_type="textarea"
current_field_value={$lq__lead_obj.exhibitor_notes} current_field_value={$lq__lead_obj.exhibitor_notes}
@@ -221,7 +221,7 @@
<Element_ae_crud_v2 <Element_ae_crud_v2
api_cfg={$ae_api} api_cfg={$ae_api}
object_type="event_exhibit_tracking" object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id} object_id={exhibit_tracking_id ?? ''}
field_name="priority" field_name="priority"
field_type="boolean" field_type="boolean"
current_field_value={$lq__lead_obj.priority} current_field_value={$lq__lead_obj.priority}
@@ -250,7 +250,7 @@
<Element_ae_crud_v2 <Element_ae_crud_v2
api_cfg={$ae_api} api_cfg={$ae_api}
object_type="event_exhibit_tracking" object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id} object_id={exhibit_tracking_id ?? ''}
field_name="enable" field_name="enable"
field_type="boolean" field_type="boolean"
current_field_value={$lq__lead_obj.enable} current_field_value={$lq__lead_obj.enable}