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:
@@ -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.
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
–
|
||||
{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
|
||||
)}
|
||||
–
|
||||
{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}
|
||||
|
||||
Reference in New Issue
Block a user