fix(pres_mgmt): restore session view — name always visible, POC in hero card, email/copy links

- session_view.svelte: session name/code were only rendered in edit_mode — non-editors
  saw a blank card. Now always visible; edit_mode just wraps them in field editors.
- Restructured hero card as a <ul> with datetime, room, and POC as rows inside the card.
  POC no longer floats below as a disconnected block.
- Dynamic POC label (label__session_poc_name) used throughout: row label, modal titles,
  fallback text, and editor label — no more hardcoded "Host:".
- POC "Select Person" flow: gate select editor on person_options_loaded to prevent empty
  dropdown on open; button reads "Reload Person" after list is loaded.
- Restored email sign-in link button in POC row with idle/sending/sent/error feedback.
  Shown when require__session_agree && show__email_access_link && poc_person_primary_email.
- Restored inline copy-access-link for trusted staff (show__copy_access_link).
- session_page_menu.svelte: fix event_session_id prop — was passing event_id instead of
  event_session_id, breaking the Sign_in_out auth grant.
- ae_comp__event_session_poc_profile.svelte: migrate run() to $effect, fix poc_person_id_random
  → poc_person_id, fix events_slct reference in copy link URL.
- +page.svelte: add pres_mgmt config sync so session pages opened directly by URL get
  correct hide__session_poc and other remote config values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-12 15:15:58 -04:00
parent 3085d1dc63
commit 45f8bb5e58
5 changed files with 470 additions and 234 deletions

View File

@@ -3,8 +3,60 @@
> **Status:** Stable — ongoing development. > **Status:** Stable — ongoing development.
> **Scope:** Active/open work only. Completed detail lives in archive files. > **Scope:** Active/open work only. Completed detail lives in archive files.
## 🔴 CMSC Charlotte — May 27 (Presentation Management) ## 🔴 LCI October — Pres Mgmt Restoration (in progress 2026-06-12)
**Post-show hardening only**
These features regressed over the last 6 months and must be working before the LCI conference.
Reference commit for original working implementation: `bb993a102`.
### Session POC (Champion/Moderator) — `session_view.svelte`
**Root cause of visible bugs:** The POC section is placed *below* the session hero card as a
separate disconnected block. In the original it was part of a structured `<ul>` with the session
name, code, datetime, location, and description all together. The current layout looks and feels
wrong to users.
- [x] **[Pres Mgmt] POC section — move inside session hero card** (2026-06-12)
Restructured hero card as a `<ul>` with datetime, room, and POC as rows inside the card.
Session name and code are now always visible (not just in edit_mode — that was a bug).
- [x] **[Pres Mgmt] POC assignment — "Select Person" flow broken** (2026-06-12)
Gated the select editor on `person_options_loaded` (`Object.keys($slct.person_obj_kv).length > 0`).
"Select Person" button renders as "Reload Person" after list is loaded.
- [x] **[Pres Mgmt] Email Session POC sign-in link — UI missing** (2026-06-12)
Restored email button in POC row with `sending/sent/error` state feedback.
Shown when `require__session_agree && show__email_access_link && poc_person_primary_email`.
- [x] **[Pres Mgmt] Copy Session POC access link — UI missing from session view** (2026-06-12)
Restored inline `MyClipboard` copy button in POC row for trusted staff.
Shown when `show__copy_access_link && trusted_access && poc_sign_in_url`.
### Presenter Sign-In
- [ ] **[Pres Mgmt] Presenter email sign-in link routes to wrong page**
`email_sign_in__event_presenter()` builds a URL to `/presenter/[id]?person_id=...&person_pass=...`.
The URL param parser (`sign_in_out.svelte`) is only mounted on the *session* page menu, not the
presenter page. A presenter clicking their email link lands on their page with no auth granted.
Fix: mount `Sign_in_out` in `presenter_page_menu.svelte` (same way session menu does it), or
change the email link to route to the session page (which already has the parser) and include
the presenter/presentation IDs as params — which is how it worked originally.
- [ ] **[Pres Mgmt] Presenter agreement not enforced before file upload**
`require__presenter_agree` is stored and displayed but the upload components are gated on
`auth__kv.presenter[id]` only, not on `presenter.agree`. A presenter who signs in but has not
agreed can still upload. The original blocked the upload section until `agree === true`.
### Session POC Sign-In
- [ ] **[Pres Mgmt] `session_page_menu.svelte` sign-in prop still wrong**
`event_session_id` prop passed to `Sign_in_out` was just changed from `event_id` to
`event_session_id` — verify this is actually `$lq__event_session_obj?.event_session_id`
(the real session ID string) not the URL param `url_session_id`. The sign-in component
uses this value to set `auth__kv.session[event_session_id]`.
---
- [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)** - [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)**
- [ ] Use timestamp/randomized temp filename so macOS always sees a new path. - [ ] Use timestamp/randomized temp filename so macOS always sees a new path.

View File

@@ -87,6 +87,23 @@ let lq__event_obj = $derived(
liveQuery(async () => await db_events.event.get(url_event_id)) liveQuery(async () => await db_events.event.get(url_event_id))
); );
// Sync server-side pres_mgmt config into local PersistedState.
// WHY: this page is often reached directly by URL without first visiting the
// pres_mgmt overview, which is the only other place this sync runs. Without
// it, pres_mgmt_loc.current keeps stale values (e.g. hide__session_poc=true)
// that make POC assignment and other edit controls invisible to staff.
$effect(() => {
const remote_cfg = $lq__event_obj?.mod_pres_mgmt_json;
if (remote_cfg) {
untrack(() => {
events_func.sync_config__event_pres_mgmt({
pres_mgmt_cfg_remote: remote_cfg,
log_lvl: 0
});
});
}
});
// $derived.by: capture only the specific ID so that unrelated store changes // $derived.by: capture only the specific ID so that unrelated store changes
// don't needlessly recreate the liveQuery observable. // don't needlessly recreate the liveQuery observable.
let lq__event_presenter_obj = $derived.by(() => { let lq__event_presenter_obj = $derived.by(() => {

View File

@@ -1,31 +1,16 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
// Imports (external and then internal) // Imports (external and then internal)
import { browser } from '$app/environment';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
// Import components and elements
// import Element_input_files_tbl from '$lib/element_input_files_tbl.svelte';
// Import storage, functions, and libraries // Import storage, functions, and libraries
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
// import { api } from '$lib/api';
import { import {
ae_snip,
ae_loc, ae_loc,
ae_sess, ae_api
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
events_sess, events_sess
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte'; import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte'; import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte';
@@ -57,32 +42,25 @@ let ae_tmp: key_val = $state({});
ae_tmp.biography = null; ae_tmp.biography = null;
let poc_type = pres_mgmt_loc.current.label__session_poc_type; let poc_type = $derived(pres_mgmt_loc.current.label__session_poc_type ?? 'poc');
let poc_name = pres_mgmt_loc.current.label__session_poc_name; let poc_name = $derived(pres_mgmt_loc.current.label__session_poc_name ?? 'Point of Contact');
run(() => { // Initialise biography from the POC kv_json on first available data.
if ( // WHY: biography lives inside poc_kv_json keyed by poc_type — not a top-level column.
browser && // We only seed the local draft once (when null) so the textarea doesn't overwrite in-progress edits.
ae_tmp.biography === null && $effect(() => {
$lq__event_session_obj?.poc_kv_json && const kv = $lq__event_session_obj?.poc_kv_json;
$lq__event_session_obj?.poc_kv_json[poc_type]?.biography if (ae_tmp.biography === null && kv && kv[poc_type]?.biography) {
) { ae_tmp.biography = kv[poc_type].biography;
ae_tmp.biography =
$lq__event_session_obj?.poc_kv_json[poc_type].biography;
console.log(`ae_tmp.biography:`, ae_tmp.biography);
} }
}); });
// $: if ($lq__event_session_obj?.poc_kv_json) {
// lq__event_session_obj.poc_kv_json = JSON.parse(lq__event_session_obj.poc_kv_json);
// }
let clipboard_success = $state(false); let clipboard_success = $state(false);
</script> </script>
<section class={class_li}> <section class={class_li}>
{#if $lq__event_session_obj} {#if $lq__event_session_obj}
{#if $lq__event_session_obj.poc_person_id_random && $ae_loc.trusted_access} {#if $lq__event_session_obj.poc_person_id && $ae_loc.trusted_access}
<span class="float-right"> <span class="float-right">
<!-- A button to copy the access link to the clipboard. --> <!-- A button to copy the access link to the clipboard. -->
<!-- Example: /events/CHs3F44Xq76/session/Wh8UnJlbIA0?person_id=fV1dl_IJ0yY&person_pass=abc123 --> <!-- Example: /events/CHs3F44Xq76/session/Wh8UnJlbIA0?person_id=fV1dl_IJ0yY&person_pass=abc123 -->
@@ -90,7 +68,7 @@ let clipboard_success = $state(false);
<!-- <span class="fas fa-copy mx-1"></span> --> <!-- <span class="fas fa-copy mx-1"></span> -->
<MyClipboard <MyClipboard
value={encodeURI( value={encodeURI(
`${$ae_loc.url_origin}/events/${$lq__event_session_obj.event_id}/session/${$events_slct.event_session_id}?person_id=${$lq__event_session_obj.poc_person_id}&person_pass=${$lq__event_session_obj.poc_person_passcode}&session_id=${$lq__event_session_obj.event_session_id}` `${$ae_loc.url_origin}/events/${$lq__event_session_obj.event_id}/session/${$lq__event_session_obj.event_session_id}?person_id=${$lq__event_session_obj.poc_person_id}&person_pass=${$lq__event_session_obj.poc_person_passcode}&session_id=${$lq__event_session_obj.event_session_id}`
)} )}
btn_text="Copy Access Link" btn_text="Copy Access Link"
btn_title="Copy the POC (moderator/champion) access link to the clipboard." btn_title="Copy the POC (moderator/champion) access link to the clipboard."

View File

@@ -19,7 +19,6 @@ import { Clock, Info, Send, Settings, ToggleLeft, ToggleRight, X } from '@lucide
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_slct } from '$lib/stores/ae_events_stores'; import { events_slct } from '$lib/stores/ae_events_stores';
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte'; import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
@@ -132,12 +131,11 @@ async function toggle_hide_launcher() {
</span> </span>
<span <span
class="ae_menu__action_options" class="ae_menu__action_options">
class:hidden={!events_auth_loc.current.auth__person?.id}>
{#if $lq__event_session_obj?.event_id} {#if $lq__event_session_obj?.event_id}
<Sign_in_out <Sign_in_out
{data} {data}
event_session_id={$lq__event_session_obj?.event_id} event_session_id={$lq__event_session_obj?.event_session_id}
{lq__event_session_obj} {lq__event_session_obj}
{lq__auth__event_presenter_obj} /> {lq__auth__event_presenter_obj} />
{/if} {/if}

View File

@@ -23,12 +23,13 @@ import { ae_util } from '$lib/ae_utils/ae_utils';
import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte'; import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte';
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte'; import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
import { core_func } from '$lib/ae_core/ae_core_functions'; import { core_func } from '$lib/ae_core/ae_core_functions';
import { ae_snip, ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_snip, ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { import {
events_sess, events_sess,
events_slct events_slct
} from '$lib/stores/ae_events_stores'; } from '$lib/stores/ae_events_stores';
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte'; import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
import { events_auth_loc } from '$lib/stores/ae_events_stores__auth.svelte';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
@@ -40,8 +41,12 @@ import {
Clock, Clock,
IdCard, IdCard,
Link, Link,
MapPin Mail,
MapPin,
Pencil,
Unlink
} from '@lucide/svelte'; } from '@lucide/svelte';
if (!$events_sess.pres_mgmt) { if (!$events_sess.pres_mgmt) {
$events_sess.pres_mgmt = {}; $events_sess.pres_mgmt = {};
$events_sess.pres_mgmt.show_modal__presenter_agree = null; $events_sess.pres_mgmt.show_modal__presenter_agree = null;
@@ -54,6 +59,9 @@ $events_sess.pres_mgmt.show_content__presenter_start = false;
// Description expand/collapse — collapsed by default (descriptions can be long) // Description expand/collapse — collapsed by default (descriptions can be long)
let desc_expanded = $state(false); let desc_expanded = $state(false);
// Email POC status — shows feedback after sending
let poc_email_status = $state<'idle' | 'sending' | 'sent' | 'error'>('idle');
// Location list for the session's event — used to build the select options in edit mode. // Location list for the session's event — used to build the select options in edit mode.
// WHY: event_location_id is a FK; the editor needs the full list so staff can pick from // WHY: event_location_id is a FK; the editor needs the full list so staff can pick from
// known rooms instead of typing a raw UUID. Keyed by event_id from the session object. // known rooms instead of typing a raw UUID. Keyed by event_id from the session object.
@@ -94,6 +102,37 @@ function to_datetime_local(raw: string | Date | null | undefined): string {
return ae_util.iso_datetime_formatter(raw, 'datetime_iso_no_seconds').replace(' ', 'T'); return ae_util.iso_datetime_formatter(raw, 'datetime_iso_no_seconds').replace(' ', 'T');
} }
// Loads account people into the shared select options map so staff can assign
// a session POC from known person records instead of typing IDs manually.
async function load_person_options_for_session_poc() {
if (!$slct.account_id) return;
$slct.person_obj_li = core_func
.load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $slct.account_id,
limit: $ae_loc.person.qry_limit__people,
order_by_li: {
family_name: 'ASC',
given_name: 'ASC',
updated_on: 'DESC',
created_on: 'DESC'
}
})
.then(function (load_results) {
if (load_results) {
let person_obj_kv: Record<string, string> = {};
person_obj_kv[''] = '-- Select a person --';
load_results.forEach((person_obj) => {
let option_text = `${person_obj?.last_first_name ?? person_obj?.given_name} (${person_obj?.primary_email?.length ? person_obj?.primary_email : '-- not set --'})`;
person_obj_kv[person_obj.person_id] = option_text;
});
$slct.person_obj_kv = person_obj_kv;
}
return load_results;
});
}
// Seed defaults from event start date (date only) so staff don't have to type // Seed defaults from event start date (date only) so staff don't have to type
// the date from scratch. Time defaults to 08:00 / 09:00 as a neutral placeholder. // the date from scratch. Time defaults to 08:00 / 09:00 as a neutral placeholder.
let event_start_date = $derived( let event_start_date = $derived(
@@ -104,6 +143,25 @@ let event_start_date = $derived(
let default_start_datetime = $derived(event_start_date ? `${event_start_date}T08:00` : ''); let default_start_datetime = $derived(event_start_date ? `${event_start_date}T08:00` : '');
let default_end_datetime = $derived(event_start_date ? `${event_start_date}T09:00` : ''); let default_end_datetime = $derived(event_start_date ? `${event_start_date}T09:00` : '');
// Derived helpers for the POC sign-in URL (used by both copy link and email).
// WHY: poc_person_passcode gates access for the session POC without requiring a full account.
let poc_sign_in_url = $derived(
$lq__event_session_obj?.poc_person_id && $lq__event_session_obj?.poc_person_passcode
? encodeURI(
`${$ae_loc.url_origin}/events/${$lq__event_session_obj.event_id}/session/${$lq__event_session_obj.event_session_id}?person_id=${$lq__event_session_obj.poc_person_id}&person_pass=${$lq__event_session_obj.poc_person_passcode}`
)
: null
);
// person_obj_kv is populated by load_person_options_for_session_poc; gate the
// select editor on it being non-empty so staff don't see a blank dropdown.
let person_options_loaded = $derived(Object.keys($slct.person_obj_kv ?? {}).length > 0);
// Whether the current session POC is signed in (auth'd) on this device.
let poc_is_authed = $derived(
events_auth_loc.current.auth__kv.session[$lq__event_session_obj?.event_session_id] === true
);
// QR Code Generation Logic // QR Code Generation Logic
$events_sess.pres_mgmt.session__updated_on = null; $events_sess.pres_mgmt.session__updated_on = null;
$effect(() => { $effect(() => {
@@ -126,38 +184,49 @@ $effect(() => {
}); });
} }
}); });
async function send_poc_email_link() {
const sess = $lq__event_session_obj;
if (!sess?.poc_person_primary_email || !sess?.poc_person_id || !sess?.poc_person_passcode) return;
poc_email_status = 'sending';
try {
await events_func.email_sign_in__event_session({
api_cfg: $ae_api,
to_email: sess.poc_person_primary_email,
to_name: sess.poc_person_full_name ?? '',
base_url: $ae_loc.url_origin,
person_id: sess.poc_person_id,
person_passcode: sess.poc_person_passcode,
event_id: sess.event_id,
event_session_id: sess.event_session_id,
session_name: sess.name
});
poc_email_status = 'sent';
setTimeout(() => { poc_email_status = 'idle'; }, 4000);
} catch {
poc_email_status = 'error';
setTimeout(() => { poc_email_status = 'idle'; }, 4000);
}
}
</script> </script>
<!-- <!--
SESSION VIEW — Speaker Ready Room & Remote Upload SESSION VIEW — Speaker Ready Room & Remote Upload
Primary users: Presenters uploading files (remote) + Staff helping presenters (onsite). Primary users: Presenters uploading files (remote) + Staff helping presenters (onsite).
Design intent: "Is this the right session?" must be answerable in <3 seconds. Design intent: "Is this the right session?" must be answerable in <3 seconds.
Show: name, time, room, host, description. Hide admin noise unless edit_mode. Show: name, time, room, POC, description. Hide admin noise unless edit_mode.
Section 508: all interactive elements labelled, focus rings, sufficient contrast. Section 508: all interactive elements labelled, focus rings, sufficient contrast.
--> -->
<section class="space-y-2"> <section class="space-y-2">
<!-- SESSION HERO: Name + Schedule + Room --> <!-- SESSION HERO CARD: Name, Code, Datetime, Room, POC — all in one block -->
<div <div class="border-surface-200-800 bg-surface-50-900 overflow-hidden rounded-xl border shadow-sm">
class=" <div class="flex flex-col gap-3 px-4 pt-4 pb-3">
border-surface-200-800 bg-surface-50-900 overflow-hidden rounded-xl border shadow-sm
flex flex-col
items-center justify-center
gap-3 px-4 py-2
transition-all
w-full
"
>
<!-- <div class="
"
> -->
<!-- QR code: floats top-right, compact by default, toggle to enlarge. <!-- QR code: floats top-right, compact by default, toggle to enlarge.
Only rendered once the async URL is resolved (string), never while Only rendered once the async URL is resolved (string), never while
it is still the boolean `true` loading placeholder. --> it is still the boolean `true` loading placeholder. -->
{#if $lq__event_session_obj && typeof $events_sess.pres_mgmt.session_qr_url?.[$lq__event_session_obj.id] === 'string'} {#if $lq__event_session_obj && typeof $events_sess.pres_mgmt.session_qr_url?.[$lq__event_session_obj.id] === 'string'}
<div <div class="float-right mb-1 ml-3 flex flex-col items-center gap-1">
class="float-right mb-1 ml-3 flex flex-col items-center gap-1">
<button <button
type="button" type="button"
onclick={() => onclick={() =>
@@ -189,11 +258,10 @@ $effect(() => {
</div> </div>
{/if} {/if}
<!-- Session Name: the primary identity check --> <!-- Session Name + Code: always visible regardless of edit_mode -->
{#if $lq__event_session_obj} {#if $lq__event_session_obj}
<div>
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div>
<Element_ae_obj_field_editor <Element_ae_obj_field_editor
object_type={'event_session'} object_type={'event_session'}
object_id={$lq__event_session_obj.id} object_id={$lq__event_session_obj.id}
@@ -205,210 +273,334 @@ $effect(() => {
api_cfg: $ae_api, api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id event_session_id: $lq__event_session_obj.id
})}> })}>
<div class="text-lg leading-snug font-bold"> <div class="text-xl leading-snug font-bold">
{$lq__event_session_obj.name} {$lq__event_session_obj.name}
</div> </div>
</Element_ae_obj_field_editor> </Element_ae_obj_field_editor>
<Element_ae_obj_field_editor {#if !pres_mgmt_loc.current.hide__session_code}
object_type="event_session" <Element_ae_obj_field_editor
object_id={$lq__event_session_obj.id} object_type="event_session"
field_name="code" object_id={$lq__event_session_obj.id}
field_type="text" field_name="code"
edit_label="Session Code" field_type="text"
current_value={$lq__event_session_obj.code} edit_label="Session Code"
placeholder="e.g. SES-101" current_value={$lq__event_session_obj.code}
on_success={() => placeholder="e.g. SES-101"
events_func.load_ae_obj_id__event_session({ on_success={() =>
api_cfg: $ae_api, events_func.load_ae_obj_id__event_session({
event_session_id: $lq__event_session_obj.id api_cfg: $ae_api,
})}> event_session_id: $lq__event_session_obj.id
})}>
<span class="badge preset-tonal-surface mt-1 text-xs"
>code: {$lq__event_session_obj.code}</span>
</Element_ae_obj_field_editor>
{/if}
{:else}
<div class="text-xl leading-snug font-bold">
{$lq__event_session_obj.name}
</div>
{#if !pres_mgmt_loc.current.hide__session_code && $lq__event_session_obj.code}
<span class="badge preset-tonal-surface mt-1 text-xs" <span class="badge preset-tonal-surface mt-1 text-xs"
>code: {$lq__event_session_obj.code}</span> >code: {$lq__event_session_obj.code}</span>
</Element_ae_obj_field_editor> {/if}
</div>
{/if} {/if}
</div>
{:else} {:else}
<!-- Skeleton placeholder while LiveQuery resolves --> <!-- Skeleton placeholder while LiveQuery resolves -->
<!-- <div class="bg-surface-200-800 h-7 w-2/3 animate-pulse rounded"> <div class="bg-surface-200-800 h-7 w-2/3 animate-pulse rounded"></div>
</div> -->
{/if} {/if}
<!-- Date/Time + Room as info chips --> <!-- Info rows: Datetime, Room, POC -->
{#if $lq__event_session_obj} {#if $lq__event_session_obj}
<div class=" <ul class="flex flex-col gap-2">
flex flex-wrap
items-center justify-center
gap-2
transition-all
max-w-xl
"
>
<div class="flex flex-wrap items-center gap-1">
<span
class="bg-primary-500/10 text-primary-700 dark:text-primary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200">
<button
type="button"
onclick={() => (pres_mgmt_loc.current.use_12h = !pres_mgmt_loc.current.use_12h)}
title={pres_mgmt_loc.current.use_12h ? 'Switch to 24-hour time' : 'Switch to 12-hour time'}
class="cursor-pointer rounded focus-visible:ring-1 focus-visible:ring-current"
aria-label={pres_mgmt_loc.current.use_12h ? 'Switch to 24-hour time' : 'Switch to 12-hour time'}>
<Clock size="1em" class="text-xs" />
</button>
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'dddd'
)},
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'datetime_long',
pres_mgmt_loc.current.use_12h
)}
&ndash;
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
'time_short',
pres_mgmt_loc.current.use_12h
)}
</span>
</div>
<!-- Datetime editors — only in edit_mode, shown as a separate row below the <!-- Date/Time row -->
display chip so they have enough room (datetime-local inputs are wide). <li class="flex flex-wrap items-center gap-2">
current_value uses to_datetime_local() to convert stored "YYYY-MM-DD HH:mm:ss" <span
→ "YYYY-MM-DDTHH:MM" required by the input. class="bg-primary-500/10 text-primary-700 dark:text-primary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200">
Falls back to event start date + placeholder time when session datetime is empty, <button
so staff only have to adjust the time rather than re-enter the full date. --> type="button"
{#if $ae_loc.edit_mode} onclick={() => (pres_mgmt_loc.current.use_12h = !pres_mgmt_loc.current.use_12h)}
<div class="flex flex-wrap gap-3"> title={pres_mgmt_loc.current.use_12h ? 'Switch to 24-hour time' : 'Switch to 12-hour time'}
<div class=""> class="cursor-pointer rounded focus-visible:ring-1 focus-visible:ring-current"
aria-label={pres_mgmt_loc.current.use_12h ? 'Switch to 24-hour time' : 'Switch to 12-hour time'}>
<Clock size="1em" class="text-xs" />
</button>
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'dddd'
)},
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'datetime_long',
pres_mgmt_loc.current.use_12h
)}
&ndash;
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
'time_short',
pres_mgmt_loc.current.use_12h
)}
</span>
<!-- Datetime editors — only in edit_mode -->
{#if $ae_loc.edit_mode}
<div class="flex flex-wrap gap-3">
<Element_ae_obj_field_editor
display_block={true}
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="start_datetime"
field_type="datetime"
edit_label="Start Date & Time"
current_value={to_datetime_local($lq__event_session_obj.start_datetime) || default_start_datetime}
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
<span class="text-xs font-semibold opacity-60">
<Clock size="0.9em" class="mr-1 inline" />Start:
{ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'datetime_long', pres_mgmt_loc.current.use_12h)}
</span>
</Element_ae_obj_field_editor>
<Element_ae_obj_field_editor
display_block={true}
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="end_datetime"
field_type="datetime"
edit_label="End Date & Time"
current_value={to_datetime_local($lq__event_session_obj.end_datetime) || default_end_datetime}
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
<span class="text-xs font-semibold opacity-60">
<Clock size="0.9em" class="mr-1 inline" />End:
{ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, 'datetime_long', pres_mgmt_loc.current.use_12h)}
</span>
</Element_ae_obj_field_editor>
</div>
{/if}
</li>
<!-- Room/Location row (hidden by config) -->
<li class:hidden={pres_mgmt_loc.current.hide__session_location}>
{#if $ae_loc.edit_mode}
<!-- Room/Location: editable in edit_mode via select from this event's locations.
WHY: event_location_id is a FK — staff need to pick from known rooms,
not type a raw UUID. The liveQuery above loads locations for this event. -->
<Element_ae_obj_field_editor <Element_ae_obj_field_editor
display_block={true} display_block={true}
object_type="event_session" object_type="event_session"
object_id={$lq__event_session_obj.id} object_id={$lq__event_session_obj.id}
field_name="start_datetime" field_name="event_location_id"
field_type="datetime" field_type="select"
edit_label="Start Date & Time" edit_label="Room / Location"
current_value={to_datetime_local($lq__event_session_obj.start_datetime) || default_start_datetime} current_value={$lq__event_session_obj.event_location_id ?? null}
allow_null={true}
select_options={location_select_options}
on_success={() => on_success={() =>
events_func.load_ae_obj_id__event_session({ events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api, api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id event_session_id: $lq__event_session_obj.id
})}> })}>
<span class="text-xs font-semibold opacity-60"> <span
<Clock size="0.9em" class="inline mr-1" />Start: class="bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200">
{ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'datetime_long', pres_mgmt_loc.current.use_12h)} <MapPin size="1em" class="text-xs" />
{$lq__event_session_obj.event_location_name ?? 'No room assigned'}
</span> </span>
</Element_ae_obj_field_editor> </Element_ae_obj_field_editor>
</div> {:else if $lq__event_session_obj.event_location_name}
<div class="">
<Element_ae_obj_field_editor
display_block={true}
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="end_datetime"
field_type="datetime"
edit_label="End Date & Time"
current_value={to_datetime_local($lq__event_session_obj.end_datetime) || default_end_datetime}
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
<span class="text-xs font-semibold opacity-60">
<Clock size="0.9em" class="inline mr-1" />End:
{ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, 'datetime_long', pres_mgmt_loc.current.use_12h)}
</span>
</Element_ae_obj_field_editor>
</div>
</div>
{/if}
<div class="flex flex-wrap items-center gap-1">
<!-- Room/Location: editable in edit_mode via select from this event's locations.
WHY: event_location_id is a FK — staff need to pick from known rooms,
not type a raw UUID. The liveQuery above loads locations for this event. -->
{#if $ae_loc.edit_mode}
<Element_ae_obj_field_editor
display_block={true}
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="event_location_id"
field_type="select"
edit_label="Room / Location"
current_value={$lq__event_session_obj.event_location_id ?? null}
allow_null={true}
select_options={location_select_options}
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
<span <span
class="bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200"> class="bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200">
<MapPin size="1em" class="text-xs" /> <MapPin size="1em" class="text-xs" />
{$lq__event_session_obj.event_location_name ?? 'No room assigned'} {$lq__event_session_obj.event_location_name}
</span> </span>
</Element_ae_obj_field_editor> {/if}
{:else if $lq__event_session_obj.event_location_name} </li>
<span
class="bg-tertiary-500/10 text-tertiary-700 dark:text-tertiary-300 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold transition-colors duration-200">
<MapPin size="1em" class="text-xs" />
{$lq__event_session_obj.event_location_name}
</span>
{/if}
</div>
</div> <!-- POC / Host row (hidden by config) -->
<li class:hidden={pres_mgmt_loc.current.hide__session_poc}>
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold opacity-60">
{pres_mgmt_loc.current.label__session_poc_name}:
</span>
{#if $lq__event_session_obj?.poc_person_id}
<!-- Profile button — only visible to trusted staff or signed-in session POC.
WHY: poc_person_full_name is PII; non-authed users just see the name as text. -->
{#if $ae_loc.trusted_access || poc_is_authed}
<button
type="button"
class="btn btn-sm preset-tonal-primary transition-colors duration-200"
onclick={() =>
($events_sess.pres_mgmt.show__session_poc_profile = true)}
aria-haspopup="dialog">
<IdCard size="1em" class="mr-1" />
{$lq__event_session_obj.poc_person_full_name}'s Profile
</button>
{:else}
<span class="text-sm"
>{$lq__event_session_obj.poc_person_full_name}</span>
{/if}
<!-- Copy POC sign-in link — trusted staff only, when feature is enabled.
WHY: allows onsite staff to paste the link into chat/email quickly. -->
{#if $ae_loc.trusted_access && pres_mgmt_loc.current.show__copy_access_link && poc_sign_in_url}
<MyClipboard
value={poc_sign_in_url}
btn_class="btn btn-sm preset-tonal-surface text-xs"
hide_icon={false}
hide_text={false}>
<Link size="1em" class="mr-1" />
Copy Link
</MyClipboard>
{/if}
<!-- Email POC sign-in link — shown when session agreement is required,
email feature is enabled, and the POC has an email on file. -->
{#if pres_mgmt_loc.current.require__session_agree && pres_mgmt_loc.current.show__email_access_link && $lq__event_session_obj?.poc_person_primary_email}
<button
type="button"
disabled={poc_email_status === 'sending'}
onclick={send_poc_email_link}
class="btn btn-sm transition-colors duration-200"
class:preset-tonal-surface={poc_email_status === 'idle'}
class:preset-tonal-warning={poc_email_status === 'sending'}
class:preset-tonal-success={poc_email_status === 'sent'}
class:preset-tonal-error={poc_email_status === 'error'}>
<Mail size="1em" class="mr-1" />
{#if poc_email_status === 'sending'}
Sending…
{:else if poc_email_status === 'sent'}
Sent!
{:else if poc_email_status === 'error'}
Error
{:else}
Email Sign-In Link
{/if}
</button>
{/if}
<!-- Agreement button — visible when require__session_agree is on
and user is trusted staff or the signed-in POC. -->
{#if pres_mgmt_loc.current.require__session_agree && ($ae_loc.trusted_access || poc_is_authed)}
<button
type="button"
disabled={!$ae_loc.trusted_access && !poc_is_authed}
class="btn btn-sm transition-colors duration-200"
class:preset-tonal-success={$lq__event_session_obj?.poc_agree}
class:preset-tonal-warning={!$lq__event_session_obj?.poc_agree}
onclick={() =>
($events_sess.pres_mgmt.show_modal__session_poc_agree = true)}>
{#if $lq__event_session_obj?.poc_agree}
<span class="mr-1 text-green-500"></span> Agreed
{:else}
<span class="mr-1 text-red-500"></span> Not yet agreed
{/if}
</button>
<Modal
title="{pres_mgmt_loc.current.label__session_poc_name}'s Consent and Release"
bind:open={$events_sess.pres_mgmt.show_modal__session_poc_agree}
autoclose={false}
placement="top-center">
<Comp_event_session_poc_form_agree
lq__event_session_obj={$lq__event_session_obj} />
{#snippet footer()}
<button
onclick={() =>
($events_sess.pres_mgmt.show_modal__session_poc_agree = false)}
class="btn preset-tonal-warning">Close</button>
{/snippet}
</Modal>
{/if}
<!-- Profile modal (same dynamic label) -->
<Modal
title="{pres_mgmt_loc.current.label__session_poc_name}'s Profile"
bind:open={$events_sess.pres_mgmt.show__session_poc_profile}
autoclose={false}>
<Comp_event_session_poc_profile
lq__event_session_obj={$lq__event_session_obj} />
{#snippet footer()}
<button
onclick={() =>
($events_sess.pres_mgmt.show__session_poc_profile = false)}
class="btn preset-tonal-warning">Close</button>
{/snippet}
</Modal>
{:else}
<span class="text-sm italic opacity-40"
>No {pres_mgmt_loc.current.label__session_poc_name} assigned</span>
{/if}
<!-- POC person selector — trusted staff in edit_mode only.
WHY: Only show the select editor after "Select Person" loads the list.
Showing an empty dropdown on editor open was confusing staff.
person_options_loaded gates rendering until the list is ready. -->
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<div class="mt-1 flex flex-wrap items-center gap-2">
{#if person_options_loaded}
<Element_ae_obj_field_editor
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="poc_person_id"
field_type="select"
edit_label="Session {pres_mgmt_loc.current.label__session_poc_name}"
current_value={$lq__event_session_obj.poc_person_id ?? null}
select_options={$slct.person_obj_kv}
allow_null={$ae_loc.administrator_access}
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
{#if $lq__event_session_obj.poc_person_id}
<a
href="/core/person/{$lq__event_session_obj.poc_person_id}"
class="text-primary-500 hover:text-primary-700 inline-flex items-center gap-1 text-xs hover:underline"
title="Open linked person record">
<Link size="1em" />
Re-link {pres_mgmt_loc.current.label__session_poc_name}
</a>
{:else}
<span class="inline-flex items-center gap-1 text-xs opacity-60">
<Unlink size="1em" />
Assign {pres_mgmt_loc.current.label__session_poc_name}
</span>
{/if}
</Element_ae_obj_field_editor>
{/if}
<button
type="button"
onclick={load_person_options_for_session_poc}
class="btn btn-xs preset-tonal-warning mt-1">
<Pencil size="1em" class="mr-1" />
{person_options_loaded ? 'Reload' : 'Select'} Person
</button>
</div>
{/if}
</div>
</li>
</ul>
{:else} {:else}
<div <div class="bg-surface-200-800 h-5 w-1/2 animate-pulse rounded-full"></div>
class="bg-surface-200-800 h-5 w-1/2 animate-pulse rounded-full">
</div>
{/if} {/if}
<!-- </div> --> </div>
</div>
<!-- Host / POC: visible when assigned and layout config allows -->
<div class:hidden={pres_mgmt_loc.current.hide__session_poc}>
{#if $lq__event_session_obj?.poc_person_id}
<div class="flex items-center gap-2">
<span class="text-sm font-semibold opacity-60">Host:</span>
<button
type="button"
class="btn btn-sm preset-tonal-primary transition-colors duration-200"
onclick={() =>
($events_sess.pres_mgmt.show__session_poc_profile = true)}
aria-haspopup="dialog">
<IdCard size="1em" class="mr-1" />
{$lq__event_session_obj.poc_person_full_name}
</button>
<Modal
title="Host Profile"
bind:open={
$events_sess.pres_mgmt.show__session_poc_profile
}>
<Comp_event_session_poc_profile
lq__event_session_obj={$lq__event_session_obj} />
{#snippet footer()}
<button
onclick={() =>
($events_sess.pres_mgmt.show__session_poc_profile = false)}
class="btn preset-tonal-warning">Close</button>
{/snippet}
</Modal>
</div>
{:else if $lq__event_session_obj && !pres_mgmt_loc.current.hide__session_poc}
<span class="text-sm italic opacity-40">No host assigned</span>
{/if}
</div> </div>
<!-- Description: collapsed by default in view mode (can be long). <!-- Description: collapsed by default in view mode (can be long).
In edit mode, show the editor directly — no collapse needed when staff are updating it. --> In edit mode, show the editor directly — no collapse needed when staff are updating it. -->
{#if $lq__event_session_obj?.description || $ae_loc.edit_mode} {#if $lq__event_session_obj?.description || $ae_loc.edit_mode}
<div <div class="border-surface-200-800 bg-surface-50-900 rounded-lg border px-4 py-3">
class="border-surface-200-800 bg-surface-50-900 rounded-lg border px-4 py-3">
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<Element_ae_obj_field_editor <Element_ae_obj_field_editor
display_block={true} display_block={true}
@@ -435,8 +627,7 @@ $effect(() => {
class="focus-visible:ring-primary-500 flex w-full items-center justify-between gap-2 rounded text-left focus-visible:ring-2" class="focus-visible:ring-primary-500 flex w-full items-center justify-between gap-2 rounded text-left focus-visible:ring-2"
onclick={() => (desc_expanded = !desc_expanded)} onclick={() => (desc_expanded = !desc_expanded)}
aria-expanded={desc_expanded}> aria-expanded={desc_expanded}>
<span <span class="text-xs font-bold tracking-wide uppercase opacity-40"
class="text-xs font-bold tracking-wide uppercase opacity-40"
>Description</span> >Description</span>
<span class="shrink-0 text-xs opacity-40"> <span class="shrink-0 text-xs opacity-40">
{#if desc_expanded} {#if desc_expanded}