Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/(launcher)/launcher_session_view.svelte
Scott Idem 182a066d38 fix(pres_mgmt): use tmp_sort_2 for presentation sort in Pres Mgmt and Launcher
Compute presentation-specific tmp_sort_1/tmp_sort_2 in specific_processor,
overriding the generic values from _process_generic_props which had two bugs:
- priority encoded as 0/1 ASC (backwards — true should sort first)
- sort stored as unpadded string ("10" < "2" lexicographically)
- start_datetime and code not included (presentation-specific fields)

New encoding: priority(inv)_sort(8-padded)_start_datetime_code[_name]
Both liveQueries (Pres Mgmt session page, Launcher session view) now use
.sortBy('tmp_sort_2') — cleaner and uses the indexed field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:20:50 -04:00

486 lines
24 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 {
slct__event_session_id?: string | null;
log_lvl?: number;
}
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view from './launcher_presenter_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use.
import Launcher_session_view_posters from './launcher_session_view_posters.svelte';
import { liveQuery } from 'dexie';
// import { core_func } from '$lib/ae_core_functions';
// import { db_core } from "$lib/db_core";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
TriangleAlert,
Archive,
Barcode,
Image,
LoaderCircle,
Monitor,
User,
Users
} from '@lucide/svelte';
// Event Session (Main View Trigger)
// WHY: $derived.by captures the id in the outer closure so Svelte tracks it as a
// reactive dependency and recreates the Observable when slct__event_session_id changes.
// Plain $derived(liveQuery(...)) does NOT work here: liveQuery's async callback runs in
// Dexie's zone where Svelte tracking is off, so $derived never sees the id read and never
// recreates the Observable when the session changes. Dexie's range-level change tracking
// then keeps the stale Observable alive — it only re-fires when data in the originally
// observed key range changes, not when a different session's data arrives.
let lq__event_session_obj = $derived.by(() => {
const id = slct__event_session_id;
return liveQuery(() => db_events.session.get(id));
});
// WHY: type_code drives poster vs. oral UI branching throughout this component.
// It was previously a prop that was never passed by the parent, so all poster
// code paths were silently dead. Deriving it here from the session object
// ensures it always reflects the current session.
let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// Event File (for a Session)
// WHY: $derived.by — same reason as lq__event_session_obj above. Without recreating
// the Observable when slct__event_session_id changes, Dexie never re-fires for the new
// session's files (they land in a different for_id range than what was originally observed).
let lq__event_file_obj_li = $derived.by(() => {
const id = slct__event_session_id;
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching files for session: ${id}`);
}
return await db_events.file
.where('for_id')
.equals(id)
.reverse()
.sortBy('created_on');
});
});
// Event Presentation
// WHY: $derived.by — captures id in the outer closure so a new Observable is
// created whenever the session changes. tmp_sort_2 encodes the full sort chain:
// priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
let lq__event_presentation_obj_li = $derived.by(() => {
const id = slct__event_session_id;
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presentations for session: ${id}`);
}
return await db_events.presentation
.where('event_session_id')
.equals(id)
.sortBy('tmp_sort_2');
});
});
// Event Presenter
// WHY: $derived.by — same reason as above.
let lq__event_presenter_obj_li = $derived.by(() => {
const id = slct__event_session_id;
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presenters for session: ${id}`);
}
return await db_events.presenter
.where('event_session_id')
.equals(id)
.sortBy('full_name');
});
});
// let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null;
// let link_to_id: null|string = null;
let ae_promises: key_val = $state({});
// $events_slct.id_li__event_presenter = [];
// await tick();
// ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({
// api_cfg: $ae_api,
// for_obj_type: 'event_session',
// for_obj_id: slct__event_session_id,
// // inc_file_li: false,
// params: {qry__enabled: 'enabled', qry__limit: 550},
// try_cache: true,
// log_lvl: 1,
// })
// .then(async function (load_results) {
// console.log(`load_results = `, load_results);
// // let event_presenter_id_li = [];
// // let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
// // for (let i = 0; i < load_results.length; i++) {
// // let event_presenter_obj = load_results[i];
// // let event_presenter_id = event_presenter_obj.event_presenter_id;
// // tmp_li.push(event_presenter_id);
// // }
// // event_presenter_id_li = tmp_li;
// // console.log(`event_presenter_id_li:`, event_presenter_id_li);
// // $events_slct.id_li__event_presenter = event_presenter_id_li;
// return load_results;
// });
</script>
<div
class="
event_launcher_session_view
relative h-full w-full
grow
space-y-1
">
<!-- <slot name="event_session_message">event session message</slot> -->
{#if $events_sess.launcher.loading__session_id_status}
<span class="absolute top-0 right-0 text-center text-sm text-gray-400">
<LoaderCircle size="1em" class="inline animate-spin" />
Loading session information...
</span>
<!-- {:else}
<span class="absolute top-0 right-0 text-sm text-center text-gray-400">
Session loaded?
</span> -->
{/if}
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
{#if type_code === 'poster'}
<!-- WHY: Poster sessions use a dedicated touch-first card-grid layout. -->
<Launcher_session_view_posters {slct__event_session_id} {log_lvl} />
{:else}
<!--
Session header: flex-col keeps datetime and name on separate rows so
the header height is predictable regardless of session name length.
Long names (300+ chars) are clamped to 2 lines; short names never
collapse the header below that height. Zero layout shift between sessions.
-->
<header
class="event_session_about flex flex-col items-stretch gap-0.5 border-b-2 border-gray-400 dark:border-gray-600">
<h3
class:hidden={!$lq__event_session_obj?.start_datetime ||
$events_loc.launcher.hide__session_datetimes}
class="event_session_datetimes text-center text-sm">
<button
type="button"
onclick={() => {
if (
$events_loc.launcher.time_format ==
'time_12_short'
) {
// $events_loc.launcher.datetime_format = 'datetime_long';
$events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_hours = 24;
} else {
$events_loc.launcher.time_format =
'time_12_short';
// $events_loc.launcher.datetime_format = 'datetime_12_long';
$events_loc.launcher.time_hours = 12;
}
}}>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'week_long'
)}</strong>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'date_long_month_day'
)}
</span>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
$events_loc.launcher.time_format
)}</strong>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
$events_loc.launcher.time_format
)}
</span>
</button>
</h3>
<span
class="flex w-full flex-row items-center justify-between gap-2">
<!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover -->
<h2
class="line-clamp-2 min-w-0 grow text-xl"
title={`Name: ${$lq__event_session_obj.name}\nType: ${$lq__event_session_obj.type_code} \nCode: ${$lq__event_session_obj.code} \nID: ${$lq__event_session_obj.event_session_id} \nStart Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'week_long')} ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, $events_loc.launcher.time_format)} \nEnd Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, $events_loc.launcher.time_format)}`}>
{$lq__event_session_obj?.name}
</h2>
{#if $lq__event_session_obj?.code}
<!-- shrink-0: code badge never gets squeezed by a long name -->
<span
class="shrink-0 p-1 text-base font-normal text-gray-500"
title="Session code {$lq__event_session_obj.code}">
<Barcode size="1em" class="inline" />
{$lq__event_session_obj?.code}
</span>
{/if}
</span>
</header>
<!-- <section class="event_session_description text-xs" class:d_none="{hide_description}">
{@html $lq__event_session_obj.description}
</section> -->
{#if $lq__event_session_obj?.file_count_all === 0}
<p class="text-center text-2xl font-bold text-red-500">
<TriangleAlert size="1em" class="inline" />
Warning
<TriangleAlert size="1em" class="inline" />
<br />
No files available show for this session.
</p>
{/if}
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list">
<div>
<div class="text-surface-600-400 text-xs">
<strong>
<Archive size="1em" class="inline" />
Session Files:
<span
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}>
({$lq__event_file_obj_li?.length}&times;)
</span>
</strong>
</div>
<!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<button type="button"
type="button" class="ae_btn btn_outline_warning btn_xs" title="Upload updated or additional files"
>
<span class="fas fa-upload"></span> Upload Session File(s)
</button>
{/if} -->
</div>
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class="flex flex-row flex-wrap items-center justify-center gap-1"
class:hidden={!$events_loc.launcher
.show_content__hidden_files &&
event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
hide_created_on={true}
show_internal_purpose_files={$events_loc.launcher
.show_content__internal_files}
show_bak_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode}
session_type={type_code || 'oral'}
open_method={type_code == 'poster'
? 'modal'
: null}
modal_title={$lq__event_session_obj?.name}
bind:modal__title={
$events_sess.launcher.modal__title
}
bind:modal__open_event_file_id={
$events_sess.launcher
.modal__open_event_file_id
}
bind:modal__event_file_obj={
$events_sess.launcher
.modal__event_file_obj
} />
<!-- <Launcher_file_cont {event_file_obj} hide_created_on={false} show_bak_download={($ae_loc.trusted_access || $events_loc.launcher.trusted_access)} open_file_as={$lq__event_session_obj.type_code} poster_title={$lq__event_session_obj.title} /> -->
<!-- <a
href="{$ae_api.base_url}/event/file/{event_file_obj.event_file_id}/download?filename={event_file_obj.filename}&key={$ae_api.account_id}"
class="btn btn-sm variant-soft-secondary m-0.5 *:hover:inline"
class:hidden={!ae_tmp.show__direct_download}
title={`Download this file:\n${event_file_obj.filename}\n[API] SHA256: ${event_file_obj.hash_sha256.slice(0, 10)}... Hosted ID: ${event_file_obj.hosted_file_id} Event File ID: ${event_file_obj.event_file_id}`}
>
<span class="fas fa-download mx-1"></span>
<div class="hidden">
Download
</div>
</a> -->
</li>
{/each}
</ul>
</section>
{/if}
<!-- <hr class="w-full border border-gray-200" /> -->
<section class="event_presentation_list">
<!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} -->
{#if $lq__event_presentation_obj_li}
<div class="text-surface-600-400 text-xs">
<strong>
{#if type_code == 'poster'}
<Image size="1em" class="inline" />
Posters:
{:else}
<Monitor size="1em" class="inline" />
Presentations:
{/if}
{#if $ae_loc.administrator_access && $ae_loc.edit_mode}
({$lq__event_presentation_obj_li?.length}&times;)
{/if}
</strong>
</div>
<!-- Maybe set max with? max-w-(--breakpoint-md) -->
<ul class="event_presentation_list max-w-full space-y-2">
{#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
<li
class="my-1 border-b-2 border-gray-300 py-1 text-center md:text-left dark:border-gray-700">
<!-- The presentation information -->
<div
class="event_presentation_datetime_name flex flex-row justify-evenly gap-4">
<!-- <div class="event_presentation_datetime_name"> -->
{#if event_presentation_obj?.start_datetime}
<span
class="event_presentation_datetime"
><strong
>{ae_util.iso_datetime_formatter(
event_presentation_obj?.start_datetime,
'time_12_short_no_leading'
)}</strong
></span>
{/if}
<span class="event_presentation_name grow"
>{event_presentation_obj?.name}</span>
<!-- </div> -->
<!-- Yes, this is kind of inefficient, but it works for now. -->
{#if $lq__event_presenter_obj_li && type_code == 'poster'}
{#each $lq__event_presenter_obj_li as event_presenter_obj, index (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<span
class="event_presentation_single_presenter text-sm text-gray-500 italic">
{#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'}
<User
size="0.85em"
class="inline" />
{$lq__event_presenter_obj_li[
index
]?.full_name}
{:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'}
<Users
size="0.85em"
class="inline" />
{$lq__event_presenter_obj_li[
index
]?.affiliations}
{:else}
--not set--
{/if}
</span>
{/if}
{/each}
{/if}
</div>
<!-- Presentation-level files -->
<Launcher_presentation_view
lq__event_presentation_obj={event_presentation_obj}
session_type={type_code} />
<!-- The presenter list -->
<!-- WHY: In poster mode, presenter names are already shown inline
in the presentation header above, so hide_name=true.
We still render Launcher_presenter_view_posters here because
some events store files at the PRESENTER level (for_id=event_presenter_id)
rather than the presentation level — particularly group/company presenters.
The component renders nothing if there are no presenter-level files,
so this has no visual cost for events that use presentation-level files. -->
{#if $lq__event_presenter_obj_li && $lq__event_presenter_obj_li.length}
<ul
class="event_presentation_presenter_list">
{#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<li
class="
hover:bg-surface-100-900 hover:border-surface-400-600
rounded-lg
border
border-transparent
p-1
transition-all
">
{#if type_code == 'poster'}
<Launcher_presenter_view_posters
lq__event_presenter_obj={event_presenter_obj}
hide_name={true} />
{:else}
<Launcher_presenter_view
lq__event_presenter_obj={event_presenter_obj}
session_type={type_code} />
{/if}
</li>
{/if}
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
{:else}
<p>No presentations available to display.</p>
{/if}
</section>
{/if}<!-- end type_code !== 'poster' -->
{:else}
<LoaderCircle size="1em" class="inline animate-spin" />
No session selected
{/if}
</div>
<style>
</style>