From bf935398809825a7f25d03d79702f6782d7c50db Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 27 Jan 2026 17:47:08 -0500 Subject: [PATCH] refactor(badges): stabilize search components and reactivity - Implemented Search Guard and store initialization in '+page.svelte' to fix loops and filtering. - Updated 'ae_comp__badge_search.svelte' to act as a pure UI component triggering versioned searches. - Refactored 'ae_comp__badge_obj_li.svelte' to use shared data streams and fixed icon build errors. --- .../[event_id]/(badges)/badges/+page.svelte | 324 +++++++++---- .../badges/ae_comp__badge_obj_li.svelte | 291 +++++------- .../badges/ae_comp__badge_search.svelte | 427 ++++-------------- 3 files changed, 421 insertions(+), 621 deletions(-) diff --git a/src/routes/events/[event_id]/(badges)/badges/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/+page.svelte index a59838bd..3a9a0129 100644 --- a/src/routes/events/[event_id]/(badges)/badges/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/+page.svelte @@ -5,100 +5,261 @@ log_lvl?: number; } - let { data, log_lvl = 0 }: Props = $props(); + let { data, log_lvl = $bindable(1) }: Props = $props(); // *** Import Svelte specific - // import { goto } from '$app/navigation'; + import { untrack } from 'svelte'; // *** Import other supporting libraries - // import { browser } from '$app/environment'; import { liveQuery } from 'dexie'; // *** Import Aether specific variables and functions - // import type { key_val } from '$lib/ae_stores'; import { ae_util } from '$lib/ae_utils/ae_utils'; - // import { core_func } from '$lib/ae_core_functions'; import { - ae_snip, ae_loc, - ae_sess, - ae_api, - ae_trig, - slct, - slct_trigger + ae_api } from '$lib/stores/ae_stores'; - // import Element_ae_crud from '$lib/element_ae_crud - // import Element_data_store from '$lib/element_data_store_v2.svelte'; - // import MyClipboard from '$lib/e_app_clipboard.svelte'; import { db_events } from '$lib/ae_events/db_events'; - // import { ae_snip, ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/ae_stores'; import { events_loc, events_sess, events_slct, events_trigger } from '$lib/stores/ae_events_stores'; - // import { events_func } from '$lib/ae_events_functions'; + import { events_func } from '$lib/ae_events_functions'; import Comp_badge_search from './ae_comp__badge_search.svelte'; import Comp_badge_obj_li from './ae_comp__badge_obj_li.svelte'; import { Modal } from 'flowbite-svelte'; import Comp_badge_create_form from './ae_comp__badge_create_form.svelte'; import Comp_badge_upload_form from './ae_comp__badge_upload_form.svelte'; + import { LoaderCircle } from 'lucide-svelte'; - // *** Variables - // let test_event_id = data.params.event_id; - // console.log(`Data Params: event_id=${test_event_id}`); - let url_test_val = data.url.searchParams.get('test_val'); - console.log(`URL test_val = ${url_test_val}`); - - import { onMount } from 'svelte'; - - // Defensively initialize properties to prevent binding to undefined - if ($events_loc.badges && typeof $events_loc.badges.qry_printed_status === 'undefined') { - $events_loc.badges.qry_printed_status = 'all'; - } - if ($events_loc.badges && typeof $events_loc.badges.qry_affiliations === 'undefined') { - $events_loc.badges.qry_affiliations = null; - } - if ($events_loc.badges && typeof $events_loc.badges.qry_sort_order === 'undefined') { - $events_loc.badges.qry_sort_order = ''; + // *** Initialization & Store Guard *** + // Ensure all search fields are initialized to prevent circular undefined triggers + if ($events_loc.badges) { + if (typeof $events_loc.badges.search_version === 'undefined') $events_loc.badges.search_version = 0; + if (typeof $events_loc.badges.qry__remote_first === 'undefined') $events_loc.badges.qry__remote_first = false; + if (typeof $events_loc.badges.fulltext_search_qry_str === 'undefined') $events_loc.badges.fulltext_search_qry_str = ''; + if (typeof $events_loc.badges.search_badge_type_code === 'undefined') $events_loc.badges.search_badge_type_code = ''; + if (typeof $events_loc.badges.qry_printed_status === 'undefined') $events_loc.badges.qry_printed_status = 'all'; + if (typeof $events_loc.badges.qry_affiliations === 'undefined') $events_loc.badges.qry_affiliations = ''; + if (typeof $events_loc.badges.qry_sort_order === 'undefined') $events_loc.badges.qry_sort_order = ''; } - let lq__event_obj = $state(null); + // Variables let show_create_badge_modal: boolean = $state(false); let show_upload_badge_modal: boolean = $state(false); let event_badge_id_li: Array = $state([]); - // let dq__where_type_id_val: string = `event_id_random`; - // let dq__where_eq_id_val: string = $events_slct?.event_id ?? ''; + let search_debounce_timer: any = null; + let last_search_id = 0; + let last_executed_key = ''; // Search Guard Key - // let lq__event_badge_obj_li = $derived(liveQuery(async () => { - // if (event_badge_id_li.length > 0) { - // let results = await db_events.badge - // .bulkGet(event_badge_id_li); + // Stable LiveQuery Pattern (Aether UI V3) + let lq__event_badge_obj_li = $derived.by(() => { + const ids = event_badge_id_li; + const event_id = $events_slct?.event_id; + + return liveQuery(async () => { + // SCENARIO 1: Specific IDs provided (Search Results) + if (Array.isArray(ids) && ids.length > 0) { + if (log_lvl) console.log(`Badge Page LQ: bulkGet ${ids.length} IDs`); + const results = await db_events.badge.bulkGet(ids); + return results.filter(item => item !== undefined); + } + + // SCENARIO 2: Fallback broad search (Only if no active filters) + if (event_id && !$events_loc.badges.fulltext_search_qry_str && $events_loc.badges.qry_printed_status === 'all' && !$events_loc.badges.qry_affiliations && !$events_loc.badges.search_badge_type_code) { + if (log_lvl) console.log(`Badge Page LQ: Fallback search for event: ${event_id}`); + return await db_events.badge + .where('event_id_random') + .equals(event_id) + .limit(50) + .sortBy('given_name'); + } - // return results; - // } else { - // let results = await db_events.badge - // .where(dq__where_type_id_val) - // .equals(dq__where_eq_id_val) - // .sortBy('given_name') - // // This should be sorted by a custom sort field + return []; + }); + }); - // return results; - // } - // })); + // Standardized Reactive Search Pattern (Aether UI V3) + // 1. Isolate dependencies into a stable derived object + let search_params = $derived({ + v: $events_loc.badges.search_version, + str: ($events_loc.badges.fulltext_search_qry_str ?? '').toLowerCase().trim(), + type: $events_loc.badges.search_badge_type_code, + printed: $events_loc.badges.qry_printed_status, + aff: ($events_loc.badges.qry_affiliations ?? '').toLowerCase().trim(), + sort: $events_loc.badges.qry_sort_order, + event_id: $events_slct?.event_id, + remote_first: $events_loc.badges.qry__remote_first + }); - // *** Functions and Logic + // 2. Controlled effect for triggering searches + $effect(() => { + const params = search_params; - // if (browser) { - // console.log('Browser environment detected.'); + if (search_debounce_timer) clearTimeout(search_debounce_timer); + search_debounce_timer = setTimeout(() => { + untrack(() => { + handle_search_refresh(params); + }); + }, 300); - // let url_test_val = data.url.searchParams.get('test_val'); - // console.log(`URL test_val = ${url_test_val}`); - // } + return () => { + if (search_debounce_timer) clearTimeout(search_debounce_timer); + }; + }); + + async function handle_search_refresh(params: any) { + // 1. Guard + const qry_key = JSON.stringify(params); + if (qry_key === last_executed_key) return; + last_executed_key = qry_key; + + const current_search_id = ++last_search_id; + const event_id = params.event_id; + const remote_first = params.remote_first; + + if (log_lvl) console.log(`[Badge Search #${current_search_id}] Refreshing (remote=${remote_first}, event=${event_id}, str=${params.str})...`); + + untrack(() => { + $events_sess.badges.search_status = 'loading'; + $events_sess.badges.search_complete = false; + }); + + const qry_str = params.str; + const type_code = params.type; + const printed_status = params.printed; + const aff_str = params.aff; + + // 2. FAST PATH: Local IDB Search + if (!remote_first) { + try { + if (event_id) { + let local_results = await db_events.badge + .where('event_id_random') + .equals(event_id) + .filter(badge => { + if (type_code && badge.badge_type_code !== type_code) return false; + + if (printed_status !== 'all') { + const is_printed = (badge.print_count ?? 0) > 0; + if (printed_status === 'printed' && !is_printed) return false; + if (printed_status === 'not_printed' && is_printed) return false; + } + + if (qry_str) { + const given_name = (badge.given_name ?? '').toLowerCase(); + const family_name = (badge.family_name ?? '').toLowerCase(); + const full_name = `${given_name} ${family_name}`.toLowerCase(); + const email = (badge.email ?? '').toLowerCase(); + const qry_string = (badge.default_qry_string ?? '').toLowerCase(); + + const match = full_name.includes(qry_str) || + given_name.includes(qry_str) || + family_name.includes(qry_str) || + email.includes(qry_str) || + qry_string.includes(qry_str); + + if (!match) return false; + } + + if (aff_str) { + const affiliations = (badge.affiliations ?? '').toLowerCase(); + if (!affiliations.includes(aff_str)) return false; + } + + return true; + }) + .toArray(); + + // Complex Sort Logic + local_results.sort((a, b) => { + switch (params.sort) { + case 'name_asc': + return (a.given_name ?? '').localeCompare(b.given_name ?? '') || (a.family_name ?? '').localeCompare(b.family_name ?? ''); + case 'name_desc': + return (b.given_name ?? '').localeCompare(a.given_name ?? '') || (b.family_name ?? '').localeCompare(a.family_name ?? ''); + case 'updated_desc': + return new Date(b.updated_on || 0).getTime() - new Date(a.updated_on || 0).getTime(); + case 'updated_asc': + return new Date(a.updated_on || 0).getTime() - new Date(b.updated_on || 0).getTime(); + case 'print_count_desc': + return (b.print_count ?? 0) - (a.print_count ?? 0); + default: + return (a.given_name ?? '').localeCompare(b.given_name ?? ''); + } + }); + + const local_ids = local_results.map(b => b.id || b.event_badge_id_random).filter(Boolean); + + if (current_search_id === last_search_id) { + if (log_lvl) console.log(`[Badge Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`); + untrack(() => { + event_badge_id_li = local_ids; + if (local_ids.length > 0) $events_sess.badges.search_status = 'done'; + }); + } + } + } catch (e) { + if (log_lvl) console.warn('Badge Fast Path failed.', e); + } + } else { + untrack(() => { + event_badge_id_li = []; + }); + } + + // 3. REVALIDATE: API Request + try { + // Map sort param to API order_by_li + let order_by_li: any = {}; + switch (params.sort) { + case 'name_asc': order_by_li = { given_name: 'ASC', family_name: 'ASC' }; break; + case 'name_desc': order_by_li = { given_name: 'DESC', family_name: 'DESC' }; break; + case 'updated_desc': order_by_li = { updated_on: 'DESC' }; break; + case 'updated_asc': order_by_li = { updated_on: 'ASC' }; break; + case 'print_count_desc': order_by_li = { print_count: 'DESC' }; break; + default: order_by_li = { given_name: 'ASC' }; + } + + const results = await events_func.search__event_badge({ + api_cfg: $ae_api, + event_id: event_id, + fulltext_search_qry_str: qry_str || null, + type_code: type_code || null, + printed_status: printed_status, + affiliations_qry_str: aff_str || null, + order_by_li: order_by_li, + limit: 150, + log_lvl: 0 + }); + + if (current_search_id === last_search_id) { + const api_results = results || []; + const api_ids = api_results.map((b: any) => b.id || b.event_badge_id_random).filter(Boolean); + + untrack(() => { + $events_sess.badge_li = api_results; + event_badge_id_li = api_ids; + $events_sess.badges.search_status = 'done'; + $events_sess.badges.search_complete = true; + }); + if (log_lvl) console.log(`[Badge Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`); + } + } catch (error) { + if (current_search_id === last_search_id) { + console.error('Badge revalidation failed:', error); + untrack(() => { + $events_sess.badges.search_status = 'error'; + $events_sess.badges.search_complete = true; + }); + } + } + } {#if show_create_badge_modal} @@ -109,7 +270,7 @@ event_id={$events_slct?.event_id ?? ''} on:success={() => { show_create_badge_modal = false; - $events_trigger.event_badge_qry = true; // Trigger a refresh of the list + $events_loc.badges.search_version++; }} on:cancel={() => (show_create_badge_modal = false)} /> @@ -125,7 +286,7 @@ event_id={$events_slct?.event_id ?? ''} on:success={() => { show_upload_badge_modal = false; - $events_trigger.event_badge_qry = true; // Trigger a refresh of the list + $events_loc.badges.search_version++; }} on:cancel={() => (show_upload_badge_modal = false)} /> @@ -136,24 +297,13 @@ Badges - - {ae_util.shorten_string({ string: lq__event_obj?.name ?? '-- not set --', max_length: 12 })} + {ae_util.shorten_string({ string: $events_slct?.event_obj?.name ?? '-- not set --', max_length: 12 })} - OSIT's Æ Events - - - @@ -201,32 +351,14 @@ {/if} - - - -{#if $events_sess?.badges?.search_status != 'loading' && $events_sess?.badges?.search_status != 'processing'} +{#if $events_sess?.badges?.search_status === 'loading' && event_badge_id_li.length === 0} +
+ +

Loading badges...

+
+{:else} -{:else} -

Loading badges...

-{/if} - - +{/if} \ No newline at end of file diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte index b56c1983..f58fe9e4 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte @@ -1,9 +1,7 @@ -
- - {#if $lq__event_badge_obj_li?.length} -
-

Results:

-
- - {$lq__event_badge_obj_li.length}× -
+
+ {#if visible_badge_obj_li === null} +
+ +

Loading badges...

+
+ {:else if visible_badge_obj_li.length > 0} +
+

Results:

+ + {visible_badge_obj_li.length}× +
- - -
    - {#each $lq__event_badge_obj_li.filter(Boolean) as event_badge_obj: Badge (event_badge_obj?.event_badge_id)} - {#if !event_badge_obj?.hide || $ae_loc.trusted_access} -
  • + {#each visible_badge_obj_li as event_badge_obj (event_badge_obj.event_badge_id_random)} +
  • -
    - {#if event_badge_obj?.print_count < 1 || $ae_loc.trusted_access} - - - {#if event_badge_obj?.hide} - - {:else} - - {/if} - {#if event_badge_obj?.print_count >= 1} - - + > +
    +
    + + {#if event_badge_obj?.hide} + + {:else} + + {/if} - {event_badge_obj?.print_count}× - {/if} - - - - {#if event_badge_obj?.full_name_override} - {event_badge_obj?.full_name_override} - {:else if event_badge_obj?.full_name} - {event_badge_obj?.full_name} - {:else if event_badge_obj?.given_name} - {event_badge_obj?.given_name} {event_badge_obj?.family_name} - {:else} - -- no name -- - {/if} - - - {:else} - - - {#if event_badge_obj?.hide} - - {:else} - - {/if} - {#if event_badge_obj?.print_count >= 1} - - - - {event_badge_obj?.print_count}× - {/if} - - - - {#if event_badge_obj?.full_name_override} - {event_badge_obj?.full_name_override} - {:else if event_badge_obj?.full_name} - {event_badge_obj?.full_name} - {:else if event_badge_obj?.given_name} - {event_badge_obj?.given_name} {event_badge_obj?.family_name} - {:else} - -- no name -- - {/if} - + + {#if event_badge_obj?.full_name_override} + {event_badge_obj?.full_name_override} + {:else if event_badge_obj?.full_name} + {event_badge_obj?.full_name} + {:else} + {event_badge_obj?.given_name} {event_badge_obj?.family_name} + {/if} - {/if} + + {#if event_badge_obj?.print_count >= 1} + + + {event_badge_obj.print_count} + + {/if} + {#if show_sensitive_fields} - | - - + + {#if $ae_loc.trusted_access} {event_badge_obj?.email} {:else} - {event_badge_obj?.email - ? event_badge_obj?.email.replace(/^(.{3}).*@/, '$1...@') - : ''} + {event_badge_obj?.email?.replace(/^(.{3}).*@/, '$1...@') ?? ''} {/if} {/if} - {#if !hide_affiliations} - | - - {event_badge_obj?.affiliations ?? '-- no affiliations --'} + + {#if !hide_affiliations && event_badge_obj.affiliations} + + + {event_badge_obj.affiliations} {/if} - {#if !hide_location} - | - - {event_badge_obj?.location ?? '-- no location --'} - - {/if} - {#if !hide_badge_type} - | - {event_badge_obj?.badge_type} - {/if} - {#if $ae_loc.edit_mode} -
    - - ID: {event_badge_obj?.event_badge_id} - - - CR: {event_badge_obj?.created_on - ? new Date(event_badge_obj.created_on).toLocaleString() - : '--'} - - - UP: {event_badge_obj?.updated_on - ? new Date(event_badge_obj.updated_on).toLocaleString() - : '--'} - - - FP: {event_badge_obj?.print_first_datetime - ? new Date( - event_badge_obj.print_first_datetime - ).toLocaleString() - : '--'} - - - LP: {event_badge_obj?.print_last_datetime - ? new Date( - event_badge_obj.print_last_datetime - ).toLocaleString() - : '--'} - - - CNT: {event_badge_obj?.print_count ?? 0} - -
    + {#if !hide_badge_type && event_badge_obj.badge_type} + + + {event_badge_obj.badge_type} + {/if}
    {#if $ae_loc.trusted_access} Review + Review + {/if} -
  • - {/if} + + + {#if $ae_loc.edit_mode} +
    + ID: {event_badge_obj?.event_badge_id} + CR: {ae_util.iso_datetime_formatter(event_badge_obj.created_on, 'datetime_iso_12_no_seconds')} + UP: {ae_util.iso_datetime_formatter(event_badge_obj.updated_on, 'datetime_iso_12_no_seconds')} + {#if event_badge_obj.print_first_datetime} + FP: {ae_util.iso_datetime_formatter(event_badge_obj.print_first_datetime, 'datetime_iso_12_no_seconds')} + {/if} + {#if event_badge_obj.print_last_datetime} + LP: {ae_util.iso_datetime_formatter(event_badge_obj.print_last_datetime, 'datetime_iso_12_no_seconds')} + {/if} +
    + {/if} + {/each}
- - {:else} -
-

No results available to show.

+
+ +

No badges found matching your criteria. Try adjusting your filters.

{/if} -
+
\ No newline at end of file diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte index 19394b1c..57aeea39 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte @@ -1,109 +1,35 @@ -
- - +
+ \ No newline at end of file