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
}: {
api_cfg: any;
event_id: string;
event_exhibit_id: string;
event_id: string | undefined;
event_exhibit_id: string | undefined;
fulltext_search_qry_str?: string | null;
qry_group?: 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}`);
}
if (!event_id || !event_exhibit_id) return [];
const search_query: any = {
q: fulltext_search_qry_str || '',
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"
>
{#if $events_sess.badges.search_status === 'loading'}
<span class="fas fa-spinner fa-spin mx-1"></span>
<LoaderCircle class="animate-spin mx-1" />
{:else}
<span class="fas fa-search mx-1"></span>
<Search class="mx-1" />
{/if}
Search
</button>
@@ -222,7 +222,7 @@
}}
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
</button>
{:else}
@@ -235,7 +235,7 @@
}}
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
</button>
{/if}
@@ -250,9 +250,9 @@
title="Toggle using the ID list or not."
>
{#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}
<span class="fas fa-toggle-off text-red-600 mr-1"></span>
<X size="1.2em" class="text-red-600 mr-1" />
{/if}
Use ID List
</button>

View File

@@ -71,23 +71,43 @@
$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_executed_key = '';
let log_lvl = 1;
// Stable LiveQuery Pattern
let lq__event_exhibit_tracking_obj_li = $derived.by(() => {
// --- NEW: Direct Reactive List Pattern ---
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 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) {
const results = await db_events.exhibit_tracking.bulkGet(ids);
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
.where('event_exhibit_id')
.equals(exhibit_id)
@@ -97,10 +117,16 @@
return [];
});
const subscription = observable.subscribe(res => {
raw_lead_li = res;
});
return () => subscription.unsubscribe();
});
// Exhibit Info
let lq__exhibit_obj = liveQuery(() => {
const lq__exhibit_obj = liveQuery(() => {
const exhibit_id = page.params.exhibit_id;
if (!exhibit_id) return undefined;
return db_events.exhibit.get(exhibit_id);
@@ -183,16 +209,18 @@
// 1. FAST PATH: Local IDB Search
if (!remote_first) {
try {
const target_exhibit_id = exhibit_id;
const target_licensee_email = params.licensee_email;
let local_results = await db_events.exhibit_tracking
.where('event_exhibit_id')
.equals(exhibit_id)
.equals(target_exhibit_id)
.filter((tracking) => {
// 1. Licensee Email Filter
if (params.licensee_email !== 'all') {
if (tracking.external_person_id !== params.licensee_email) return false;
if (target_licensee_email !== 'all') {
if (tracking.external_person_id !== target_licensee_email) return false;
}
// 2. Text Search Filter
if (qry_str) {
const name = (
tracking.event_badge_full_name ?? ''
@@ -284,12 +312,16 @@
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({
api_cfg: $ae_api,
event_id: page.params.event_id || '',
event_exhibit_id: exhibit_id,
event_id: q_event_id,
event_exhibit_id: q_exhibit_id,
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,
limit: 150
});
@@ -463,7 +495,7 @@
<p class="text-xl">Searching leads...</p>
</div>
{: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}
</div>
{:else if active_tab === 'manage'}

View File

@@ -25,11 +25,11 @@
</script>
<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">
<span class="fas fa-spinner fa-spin fa-2x opacity-20"></span>
</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">
<p class="text-xl opacity-50">No leads found yet.</p>
<p class="text-sm opacity-50 mt-2">
@@ -40,12 +40,12 @@
<div class="space-y-4">
<div class="flex justify-between items-center px-2">
<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>
</div>
<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
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"

View File

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