Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/+page.svelte
Scott Idem bd39fd3061 Restore Event Session search stability and advance platform-wide string ID standardization
- Restored Event Session search by standardizing on 'event_id' for Dexie queries and implementing dual-layer filtering (local + API guard) to prevent broad results from clobbering filtered views.
- Advanced String-Only ID Standardization (Phase 2) by updating generic object processors across all event library modules to support both base IDs and legacy '_random' variants.
- Refactored Event Presenter and Presentation components to support standardized '_id_li' props while maintaining backward compatibility.
- Standardized common helper identifiers to snake_case (e.g., 'prevent_default') in the Events module.
- Verified Staff and Poster email notification logic in the Bulletin Board module.
- Updated .gitignore and cleaned up test artifacts.
2026-02-04 19:32:17 -05:00

525 lines
23 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
/** @type {import('./$types').PageData} */
data: any;
log_lvl?: number;
}
let { data, log_lvl = $bindable(0) }: Props = $props();
// *** Import Svelte specific
import { untrack } from 'svelte';
import { browser } from '$app/environment';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Comp_event_session_obj_li_wrapper from '../ae_comp__event_session_obj_li_wrapper.svelte';
import { liveQuery } from 'dexie';
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/stores/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 Comp_event_files_upload from '../ae_comp__event_files_upload.svelte';
import Element_manage_event_file_li_wrap from '$lib/elements/element_manage_event_file_li_direct.svelte';
import Event_page_menu from './event_page_menu.svelte';
// Quickly save the data passed from the parent(s) to the Svelte stores, localStorage, and other.
let ae_acct = data[$slct.account_id];
let event_id = data.params.event_id;
$events_slct.event_id = ae_acct.slct.event_id;
// *** Initialization & Store Guard ***
if ($events_loc.pres_mgmt) {
if (typeof $events_loc.pres_mgmt.search_version === 'undefined') $events_loc.pres_mgmt.search_version = 0;
if (typeof $events_loc.pres_mgmt.qry__remote_first === 'undefined') $events_loc.pres_mgmt.qry__remote_first = false;
if (typeof $events_loc.pres_mgmt.fulltext_search_qry_str === 'undefined') $events_loc.pres_mgmt.fulltext_search_qry_str = '';
if (typeof $events_loc.pres_mgmt.location_name_qry_str === 'undefined') $events_loc.pres_mgmt.location_name_qry_str = '';
}
let lq__event_obj = $derived(
liveQuery(async () => {
if (log_lvl) console.log(`*** LiveQuery: lq__event_obj *** event_id=${$events_slct.event_id}`);
return await db_events.event.get($events_slct?.event_id ?? '');
})
);
// JSON formatted configuration options for an event, and specifically for the presentation management module.
$effect(() => {
if ($lq__event_obj?.mod_pres_mgmt_json) {
events_func.sync_config__event_pres_mgmt({
pres_mgmt_cfg_remote: $lq__event_obj.mod_pres_mgmt_json,
pres_mgmt_cfg_local: $events_loc.pres_mgmt,
log_lvl: log_lvl
});
}
});
let event_session_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null;
let last_search_id = 0;
let last_executed_key = ''; // Search Guard Key
// Stable LiveQuery Pattern (Aether UI V3)
let lq__event_session_obj_li = $derived.by(() => {
const ids = event_session_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(`Session Page LQ: bulkGet ${ids.length} IDs`);
const results = await db_events.session.bulkGet(ids);
return results.filter(item => item !== undefined);
}
// SCENARIO 2: Fallback broad search (Only if no active filters)
if (event_id && !$events_loc.pres_mgmt.fulltext_search_qry_str && !$events_loc.pres_mgmt.location_name_qry_str) {
if (log_lvl) console.log(`Session Page LQ: Fallback search for event: ${event_id}`);
return await db_events.session
.where('event_id')
.equals(event_id)
.limit(50)
.sortBy('name');
}
return [];
});
});
let lq__event_location_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.location
.where('event_id')
.equals($events_slct.event_id)
.sortBy('name');
return results;
})
);
// Standardized Reactive Search Pattern (Aether UI V3)
let search_params = $derived({
v: $events_loc.pres_mgmt.search_version,
str: ($events_loc.pres_mgmt.fulltext_search_qry_str ?? '').toLowerCase().trim(),
location: $events_loc.pres_mgmt.location_name_qry_str,
event_id: $events_slct?.event_id,
remote_first: $events_loc.pres_mgmt.qry__remote_first
});
$effect(() => {
const params = search_params;
if (search_debounce_timer) clearTimeout(search_debounce_timer);
search_debounce_timer = setTimeout(() => {
handle_search_refresh(params);
}, 300);
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(`[Session Search #${current_search_id}] Refreshing (remote=${remote_first}, event=${event_id}, str=${params.str})...`);
untrack(() => {
$events_sess.pres_mgmt.status_qry__search = 'loading';
});
const qry_str = params.str;
const location_name = params.location;
// 2. FAST PATH: Local IDB Search
if (!remote_first) {
try {
if (event_id) {
let local_results = await db_events.session
.where('event_id')
.equals(event_id)
.filter(session => {
if (location_name && session.event_location_name !== location_name) return false;
if (qry_str) {
const name = (session.name ?? '').toLowerCase();
const code = (session.code ?? '').toLowerCase();
const description = (session.description ?? '').toLowerCase();
const qry_string = (session.default_qry_str ?? '').toLowerCase();
const match = name.includes(qry_str) ||
code.includes(qry_str) ||
description.includes(qry_str) ||
qry_string.includes(qry_str);
if (!match) return false;
}
return true;
})
.toArray();
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
const local_ids = local_results.map(s => s.id || s.event_session_id).filter(Boolean);
if (current_search_id === last_search_id) {
if (log_lvl) console.log(`[Session Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`);
untrack(() => {
event_session_id_li = local_ids;
if (local_ids.length > 0) $events_sess.pres_mgmt.status_qry__search = 'done';
});
}
}
} catch (e) {
if (log_lvl) console.warn('Session Fast Path failed.', e);
}
} else {
untrack(() => {
event_session_id_li = [];
});
}
// 3. REVALIDATE: API Request
try {
const results = await events_func.search__event_session({
api_cfg: $ae_api,
event_id: event_id,
fulltext_search_qry_str: qry_str || null,
like_search_qry_str: qry_str || null,
location_name: location_name || null,
enabled: $events_loc.pres_mgmt.qry_enabled ?? 'enabled',
hidden: $events_loc.pres_mgmt.qry_hidden ?? 'not_hidden',
limit: $events_loc.pres_mgmt.qry_limit__sessions ?? 100,
try_cache: true,
log_lvl: 0
});
if (current_search_id === last_search_id) {
let api_results = results || [];
// Client-side Filter Guard: Ensure API results match local criteria (Backup filter)
api_results = api_results.filter(session => {
if (location_name && session.event_location_name !== location_name) return false;
if (qry_str) {
const name = (session.name ?? '').toLowerCase();
const code = (session.code ?? '').toLowerCase();
const description = (session.description ?? '').toLowerCase();
const qry_string = (session.default_qry_str ?? '').toLowerCase();
const match = name.includes(qry_str) ||
code.includes(qry_str) ||
description.includes(qry_str) ||
qry_string.includes(qry_str);
if (!match) return false;
}
return true;
});
const api_ids = api_results.map((s: any) => s.id || s.event_session_id).filter(Boolean);
untrack(() => {
$events_slct.event_session_obj_li = api_results;
event_session_id_li = api_ids;
$events_sess.pres_mgmt.status_qry__search = 'done';
});
if (log_lvl) console.log(`[Session Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`);
}
} catch (error) {
if (current_search_id === last_search_id) {
console.error('Session revalidation failed:', error);
untrack(() => {
$events_sess.pres_mgmt.status_qry__search = 'error';
});
}
}
}
if ($events_loc.pres_mgmt?.save_search_text && $events_loc.pres_mgmt?.saved_search__session) {
$events_loc.pres_mgmt.fulltext_search_qry_str =
$events_loc.pres_mgmt.saved_search__session;
}
if (
$events_loc.pres_mgmt?.save_search_text &&
$events_loc.pres_mgmt?.saved_search__session_location_name
) {
$events_loc.pres_mgmt.location_name_qry_str =
$events_loc.pres_mgmt.saved_search__session_location_name;
}
function handle_search_trigger() {
$events_loc.pres_mgmt.search_version++;
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<svelte:head>
<title>
&AElig;:
{ae_util.shorten_string({ string: $lq__event_obj?.name, max_length: 12 })}
- Pres Mgmt - {$events_loc?.title}
</title>
</svelte:head>
<Event_page_menu {lq__event_obj} />
{#if !$lq__event_obj}
<div class="flex items-center justify-center p-10 opacity-50">
<span class="fas fa-spinner fa-spin mx-1 text-2xl"></span>
<span>Loading event information...</span>
</div>
{:else if $lq__event_obj?.enable || $ae_loc.trusted_access}
<header class="ae_module_header">
<span class="flex flex-row flex-wrap gap-1 items-center justify-center">
<span class="fas fa-calendar-day m-1 text-neutral-800/80"></span>
<button type="button"
onclick={() => {
if ($events_loc.pres_mgmt.show_content__event_view == 'manage_files') {
$events_loc.pres_mgmt.show_content__event_view = null;
} else {
$events_loc.pres_mgmt.show_content__event_view = 'manage_files';
}
}}
class="btn ae_btn_secondary"
class:preset-filled-secondary-500={$events_loc.pres_mgmt.show_content__event_view == 'manage_files'}
class:preset-filled-tertiary-500={$events_loc.pres_mgmt.show_content__event_view != 'manage_files'}
class:hidden={!$ae_loc.administrator_access}
title="View event search or manage files for the event"
>
{#if $events_loc.pres_mgmt.show_content__event_view == 'manage_files'}
<span class="fas fa-search m-1"></span>
Event Search?
{:else}
<span class="fas fa-file-archive m-1"></span>
Event Files?
<span
class="badge preset-tonal-success"
class:hidden={!$lq__event_obj?.file_count}
>
<span class="fas fa-file-alt m-1"></span>
{$lq__event_obj?.file_count}×
</span>
{/if}
</button>
</span>
<h2 class="text-2xl font-bold text-center max-w-xs sm:max-w-lg md:max-w-2xl">
<span class="sm:inline-block md:hidden">
{$lq__event_obj.cfg_json?.short_name ?? $lq__event_obj?.name}
</span>
<span class="hidden md:inline-block lg:hidden">
{$lq__event_obj.cfg_json?.med_name ?? $lq__event_obj?.name}
</span>
<span class="hidden lg:inline-block">
{$lq__event_obj.cfg_json?.long_name ?? $lq__event_obj?.name}
</span>
</h2>
</header>
{#if !$events_loc.pres_mgmt.show_content__event_view || $events_loc.pres_mgmt.show_content__event_view == 'default'}
<div class="ae_container_actions">
<form
onsubmit={prevent_default(() => handle_search_trigger())}
autocomplete="off"
class="form grow flex flex-row flex-wrap gap-1 justify-center items-center w-full"
>
<button type="button"
class="btn btn-sm mx-1 ae_btn_warning"
class:hidden={!$ae_loc.authenticated_access}
onclick={() => {
$events_loc.pres_mgmt.location_name_qry_str = '';
$events_loc.pres_mgmt.show_content__session_search_room_name = !$events_loc.pres_mgmt.show_content__session_search_room_name;
handle_search_trigger();
}}
title="Search by location name"
>
<span class="fas fa-search-location"></span>
</button>
<select
name="location_name_list"
id="session_location_name_list"
bind:value={$events_loc.pres_mgmt.location_name_qry_str}
class="input text-xs font-bold font-mono min-w-fit w-min max-w-40 transition-all mx-1"
class:hidden={!$ae_loc.authenticated_access || !$events_loc.pres_mgmt.show_content__session_search_room_name}
onchange={() => handle_search_trigger()}
title="Select to filter based on the location/room name"
>
{#if $lq__event_location_obj_li}
<option value="">Location / Room</option>
{#each $lq__event_location_obj_li as event_location_obj}
<option value={event_location_obj?.name}>{event_location_obj.name}</option>
{/each}
{/if}
</select>
<button type="button"
onclick={() => {
$events_loc.pres_mgmt.fulltext_search_qry_str = '';
handle_search_trigger();
}}
class:hidden={!$events_loc.pres_mgmt.fulltext_search_qry_str}
class="btn btn-sm mx-1 ae_btn_warning"
title="Clear search text"
>
<span class="fas fa-remove-format"></span>
</button>
<input
type="search"
placeholder="Search for a session"
id="session_fulltext_search_qry_str"
bind:value={$events_loc.pres_mgmt.fulltext_search_qry_str}
class="input text-1xl hover:text-2xl font-bold font-mono w-80 transition-all mx-1 ae_btn_info"
onkeyup={(e) => {
if (e.key === 'Enter') handle_search_trigger();
}}
autofocus
data-ignore="true"
/>
<button
type="submit"
class="btn btn-lg text-2xl font-bold w-48 mx-1 ae_btn_primary"
title="Search for a session"
>
{#if $events_sess.pres_mgmt.status_qry__search == 'loading'}
<span class="fas fa-spinner fa-spin mx-1 text-success-800-200"></span>
{:else}
<span class="fas fa-search mx-1 text-neutral-800/80"></span>
{/if}
Search
</button>
{#if $ae_loc.edit_mode}
<label class="flex items-center gap-1 cursor-pointer bg-surface-200-800 px-2 py-1 rounded-token text-xs font-semibold opacity-70 hover:opacity-100 transition-all">
<span> Remote First </span>
<input
type="checkbox"
bind:checked={$events_loc.pres_mgmt.qry__remote_first}
onchange={() => handle_search_trigger()}
class="checkbox checkbox-sm"
/>
</label>
{/if}
</form>
</div>
{#if event_session_id_li.length}
<Comp_event_session_obj_li_wrapper
lq__event_session_obj_li={lq__event_session_obj_li}
hide__session_location={$events_loc.pres_mgmt?.hide__session_li_location_field}
hide__session_poc={$events_loc.pres_mgmt?.hide__session_li_poc_field}
hide__launcher_link_legacy={$events_loc.pres_mgmt?.hide__launcher_link_legacy}
hide__launcher_link={$events_loc.pres_mgmt?.hide__launcher_link}
hide__location_link={$events_loc.pres_mgmt?.hide__location_link}
log_lvl={1}
/>
{:else if $events_sess.pres_mgmt.status_qry__search === 'loading'}
<div class="flex flex-col items-center justify-center p-20 opacity-50 text-center">
<span class="fas fa-spinner fa-spin text-4xl mb-4"></span>
<p class="text-xl">Searching sessions...</p>
</div>
{:else}
<section class="text-center text-2xl bg-yellow-100 p-4 rounded-md lg:max-w-lg space-y-2 mx-auto">
<div>
<span class="fas fa-exclamation-triangle text-2xl text-yellow-500"></span>
<strong>No results to show</strong>
<span class="fas fa-exclamation-triangle text-2xl text-yellow-500"></span>
<br />
<div class="text-lg">Please use the search above to find your session.</div>
</div>
<div>
<strong>Search by:</strong>
<ul class="list-disc list-inside text-lg text-left">
<li>Session name</li>
<li>Session description</li>
<li>Presentation name</li>
<li>Presenter names</li>
<li>Presenter ID (member ID)</li>
</ul>
</div>
</section>
{/if}
{:else if $events_loc.pres_mgmt.show_content__event_view == 'manage_files' && $ae_loc.trusted_access}
{#if $lq__event_obj}
<header>
<h2 class="h3 text-center">{$lq__event_obj?.name}</h2>
<h3 class="h4 text-center">Event - Manage Files</h3>
</header>
{/if}
<div>
<h3 class="h5 text-center">
<span class="fas fa-tasks m-1 text-neutral-800/80"></span>
<span class="fas fa-mail-bulk m-1 text-neutral-800/80"></span>
Manage and Upload Event Files:
</h3>
<Comp_event_files_upload
class_li="border border-gray-300 rounded-md p-2 bg-gray-100 hover:bg-gray-200"
link_to_type="event"
link_to_id={$lq__event_obj?.event_id}
>
{#snippet label()}
<span>
<div class="text-lg">
<span class="fas fa-upload"></span>
<strong class="">Upload global event files only!</strong>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 italic">
<strong>Global event files only</strong><br />
Recommended: PowerPoint (pptx) or Keynote (key)<br />
Media: Audio and videos files should be directly embedded in PowerPoint (PPTX)
files<br />
Supplemental files: mp4, PDF, Word Doc, Excel, txt, etc
</div>
</span>
{/snippet}
</Comp_event_files_upload>
<div class="overflow-x-auto w-max max-w-full">
<Element_manage_event_file_li_wrap
link_to_type={'event'}
link_to_id={$lq__event_obj?.event_id}
allow_basic={$events_loc.auth__kv.session[$lq__event_obj?.event_id]}
allow_moderator={$events_loc.auth__kv.session[$lq__event_obj?.event_id]}
container_class_li={''}
/>
</div>
</div>
{/if}
{:else}
<div class="bg-red-100 p-4 border border-red-200 rounded-md">
<h2 class="h3">
<span class="fas fa-exclamation-triangle text-red-500 m-1"></span>
Event Disabled
</h2>
<p>This event is currently disabled. Please contact the event organizer for more information.</p>
</div>
{/if}
<style lang="postcss">
</style>