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.
> **Scope:** Active/open work only. Completed detail lives in archive files.
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Post-show hardening only**
## 🔴 LCI October — Pres Mgmt Restoration (in progress 2026-06-12)
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)**
- [ ] 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))
);
// 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
// don't needlessly recreate the liveQuery observable.
let lq__event_presenter_obj = $derived.by(() => {

View File

@@ -1,31 +1,16 @@
<script lang="ts">
import { run } from 'svelte/legacy';
// Imports (external and then internal)
import { browser } from '$app/environment';
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 type { key_val } from '$lib/stores/ae_stores';
// import { api } from '$lib/api';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
ae_api
} from '$lib/stores/ae_stores';
import {
events_sess,
events_slct,
events_trigger
events_sess
} from '$lib/stores/ae_events_stores';
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';
@@ -57,32 +42,25 @@ let ae_tmp: key_val = $state({});
ae_tmp.biography = null;
let poc_type = pres_mgmt_loc.current.label__session_poc_type;
let poc_name = pres_mgmt_loc.current.label__session_poc_name;
let poc_type = $derived(pres_mgmt_loc.current.label__session_poc_type ?? 'poc');
let poc_name = $derived(pres_mgmt_loc.current.label__session_poc_name ?? 'Point of Contact');
run(() => {
if (
browser &&
ae_tmp.biography === null &&
$lq__event_session_obj?.poc_kv_json &&
$lq__event_session_obj?.poc_kv_json[poc_type]?.biography
) {
ae_tmp.biography =
$lq__event_session_obj?.poc_kv_json[poc_type].biography;
console.log(`ae_tmp.biography:`, ae_tmp.biography);
// Initialise biography from the POC kv_json on first available data.
// WHY: biography lives inside poc_kv_json keyed by poc_type — not a top-level column.
// We only seed the local draft once (when null) so the textarea doesn't overwrite in-progress edits.
$effect(() => {
const kv = $lq__event_session_obj?.poc_kv_json;
if (ae_tmp.biography === null && kv && kv[poc_type]?.biography) {
ae_tmp.biography = kv[poc_type].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);
</script>
<section class={class_li}>
{#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">
<!-- A button to copy the access link to the clipboard. -->
<!-- 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> -->
<MyClipboard
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_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 { events_slct } from '$lib/stores/ae_events_stores';
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 { api } from '$lib/api/api';
@@ -132,12 +131,11 @@ async function toggle_hide_launcher() {
</span>
<span
class="ae_menu__action_options"
class:hidden={!events_auth_loc.current.auth__person?.id}>
class="ae_menu__action_options">
{#if $lq__event_session_obj?.event_id}
<Sign_in_out
{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__auth__event_presenter_obj} />
{/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 MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
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 {
events_sess,
events_slct
} from '$lib/stores/ae_events_stores';
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 { db_events } from '$lib/ae_events/db_events';
@@ -40,8 +41,12 @@ import {
Clock,
IdCard,
Link,
MapPin
Mail,
MapPin,
Pencil,
Unlink
} from '@lucide/svelte';
if (!$events_sess.pres_mgmt) {
$events_sess.pres_mgmt = {};
$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)
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.
// 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.
@@ -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');
}
// 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
// the date from scratch. Time defaults to 08:00 / 09:00 as a neutral placeholder.
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_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
$events_sess.pres_mgmt.session__updated_on = null;
$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>
<!--
SESSION VIEW — Speaker Ready Room & Remote Upload
Primary users: Presenters uploading files (remote) + Staff helping presenters (onsite).
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 class="space-y-2">
<!-- SESSION HERO: Name + Schedule + Room -->
<div
class="
border-surface-200-800 bg-surface-50-900 overflow-hidden rounded-xl border shadow-sm
<!-- SESSION HERO CARD: Name, Code, Datetime, Room, POC — all in one block -->
<div class="border-surface-200-800 bg-surface-50-900 overflow-hidden rounded-xl border shadow-sm">
<div class="flex flex-col gap-3 px-4 pt-4 pb-3">
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.
Only rendered once the async URL is resolved (string), never while
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'}
<div
class="float-right mb-1 ml-3 flex flex-col items-center gap-1">
<div class="float-right mb-1 ml-3 flex flex-col items-center gap-1">
<button
type="button"
onclick={() =>
@@ -189,11 +258,10 @@ $effect(() => {
</div>
{/if}
<!-- Session Name: the primary identity check -->
<!-- Session Name + Code: always visible regardless of edit_mode -->
{#if $lq__event_session_obj}
<div>
{#if $ae_loc.edit_mode}
<div>
<Element_ae_obj_field_editor
object_type={'event_session'}
object_id={$lq__event_session_obj.id}
@@ -205,210 +273,334 @@ $effect(() => {
api_cfg: $ae_api,
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}
</div>
</Element_ae_obj_field_editor>
<Element_ae_obj_field_editor
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="code"
field_type="text"
edit_label="Session Code"
current_value={$lq__event_session_obj.code}
placeholder="e.g. SES-101"
on_success={() =>
events_func.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: $lq__event_session_obj.id
})}>
{#if !pres_mgmt_loc.current.hide__session_code}
<Element_ae_obj_field_editor
object_type="event_session"
object_id={$lq__event_session_obj.id}
field_name="code"
field_type="text"
edit_label="Session Code"
current_value={$lq__event_session_obj.code}
placeholder="e.g. SES-101"
on_success={() =>
events_func.load_ae_obj_id__event_session({
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"
>code: {$lq__event_session_obj.code}</span>
</Element_ae_obj_field_editor>
</div>
{/if}
{/if}
</div>
{:else}
<!-- Skeleton placeholder while LiveQuery resolves -->
<!-- <div class="bg-surface-200-800 h-7 w-2/3 animate-pulse rounded">
</div> -->
<div class="bg-surface-200-800 h-7 w-2/3 animate-pulse rounded"></div>
{/if}
<!-- Date/Time + Room as info chips -->
<!-- Info rows: Datetime, Room, POC -->
{#if $lq__event_session_obj}
<div class="
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>
<ul class="flex flex-col gap-2">
<!-- Datetime editors — only in edit_mode, shown as a separate row below the
display chip so they have enough room (datetime-local inputs are wide).
current_value uses to_datetime_local() to convert stored "YYYY-MM-DD HH:mm:ss"
→ "YYYY-MM-DDTHH:MM" required by the input.
Falls back to event start date + placeholder time when session datetime is empty,
so staff only have to adjust the time rather than re-enter the full date. -->
{#if $ae_loc.edit_mode}
<div class="flex flex-wrap gap-3">
<div class="">
<!-- Date/Time row -->
<li class="flex flex-wrap items-center gap-2">
<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>
<!-- 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
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}
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 class="text-xs font-semibold opacity-60">
<Clock size="0.9em" class="inline mr-1" />Start:
{ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'datetime_long', pres_mgmt_loc.current.use_12h)}
<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 ?? 'No room assigned'}
</span>
</Element_ae_obj_field_editor>
</div>
<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
})}>
{:else if $lq__event_session_obj.event_location_name}
<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 ?? 'No room assigned'}
{$lq__event_session_obj.event_location_name}
</span>
</Element_ae_obj_field_editor>
{:else if $lq__event_session_obj.event_location_name}
<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>
{/if}
</li>
</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}
<div
class="bg-surface-200-800 h-5 w-1/2 animate-pulse rounded-full">
</div>
<div class="bg-surface-200-800 h-5 w-1/2 animate-pulse rounded-full"></div>
{/if}
<!-- </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).
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}
<div
class="border-surface-200-800 bg-surface-50-900 rounded-lg border px-4 py-3">
<div class="border-surface-200-800 bg-surface-50-900 rounded-lg border px-4 py-3">
{#if $ae_loc.edit_mode}
<Element_ae_obj_field_editor
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"
onclick={() => (desc_expanded = !desc_expanded)}
aria-expanded={desc_expanded}>
<span
class="text-xs font-bold tracking-wide uppercase opacity-40"
<span class="text-xs font-bold tracking-wide uppercase opacity-40"
>Description</span>
<span class="shrink-0 text-xs opacity-40">
{#if desc_expanded}