leads: UX improvements — manage tab, sign-in flow, notes editor, filter
- leads_api_access toggle in Admin Tools (manager only) - Account Status section for end users (payment/licenses/API badges + CSV export button) - Sign-out fix: use Object.fromEntries instead of delete on PersistedState proxy - Shared passcode sign-in redirects directly to Manage tab (their role is config, not capture) - Manage tab section reorder: Account Status → Lead Retrieval Config → Booth Profile → Access & Security → App Settings - Filter dropdown: replace abstract "My Leads" with direct identity options (All / Booth (Shared) / per-licensee); auto-resolves and migrates stale 'my' values - Lead detail: replace Element_ae_obj_field_editor notes with direct TipTap editor + Save Notes button; Add Notes button on empty state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -187,7 +187,7 @@ function toggle_edit() {
|
|||||||
class="badge {display_value
|
class="badge {display_value
|
||||||
? 'variant-filled-success'
|
? 'variant-filled-success'
|
||||||
: 'variant-soft-surface'}">
|
: 'variant-soft-surface'}">
|
||||||
{display_value ? 'Enabled' : 'Disabled'}
|
{display_value ? 'True' : 'False'}
|
||||||
</span>
|
</span>
|
||||||
{:else if field_type === 'tiptap'}
|
{:else if field_type === 'tiptap'}
|
||||||
<div class="prose dark:prose-invert max-w-none">
|
<div class="prose dark:prose-invert max-w-none">
|
||||||
@@ -260,7 +260,7 @@ function toggle_edit() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={draft_value}
|
bind:checked={draft_value}
|
||||||
class="checkbox" />
|
class="checkbox" />
|
||||||
<span>{draft_value ? 'Enabled' : 'Disabled'}</span>
|
<span>{draft_value ? 'True' : 'False'}</span>
|
||||||
</label>
|
</label>
|
||||||
{:else if field_type === 'tiptap'}
|
{:else if field_type === 'tiptap'}
|
||||||
<AE_Comp_Editor_TipTap
|
<AE_Comp_Editor_TipTap
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { db_events } from '$lib/ae_events/db_events';
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
import {
|
import { events_sess } from '$lib/stores/ae_events_stores';
|
||||||
events_sess,
|
|
||||||
events_slct
|
|
||||||
} from '$lib/stores/ae_events_stores';
|
|
||||||
import { leads_loc } from '$lib/stores/ae_events_stores__leads.svelte';
|
import { leads_loc } from '$lib/stores/ae_events_stores__leads.svelte';
|
||||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
@@ -14,7 +11,6 @@ import { ae_util } from '$lib/ae_utils/ae_utils';
|
|||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Download,
|
Download,
|
||||||
LayoutGrid,
|
|
||||||
List as ListIcon,
|
List as ListIcon,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -148,9 +144,9 @@ let stripe_cfg = $derived({
|
|||||||
let search_params = $derived.by(() => {
|
let search_params = $derived.by(() => {
|
||||||
let licensee_email = leads_loc.current.tracking__qry__licensee_email;
|
let licensee_email = leads_loc.current.tracking__qry__licensee_email;
|
||||||
|
|
||||||
// Resolve "My Leads" to the correct identity used when storing leads.
|
// 'my' is a legacy value — the search component now resolves to the real identity
|
||||||
// Shared-passcode users store 'shared_passcode' literal (not the passcode string itself).
|
// on mount and migrates stale persisted values. This block is a silent fallback
|
||||||
// Licensed users store their email. Aether bypass users store $ae_loc.access_type.
|
// for any session that still has 'my' before the component has had a chance to run.
|
||||||
if (licensee_email === 'my') {
|
if (licensee_email === 'my') {
|
||||||
const kv = leads_loc.current.auth_exhibit_kv?.[page.params.exhibit_id ?? ''];
|
const kv = leads_loc.current.auth_exhibit_kv?.[page.params.exhibit_id ?? ''];
|
||||||
licensee_email = kv?.type === 'shared'
|
licensee_email = kv?.type === 'shared'
|
||||||
|
|||||||
@@ -108,13 +108,12 @@ function complete_signin(key: string, type: string) {
|
|||||||
updated_on: new Date().toISOString()
|
updated_on: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Also update session passcode if shared mode
|
|
||||||
if (type === 'shared') {
|
if (type === 'shared') {
|
||||||
$events_sess.leads.entered_passcode = key;
|
$events_sess.leads.entered_passcode = key;
|
||||||
|
// Shared passcode users land on Manage — their purpose is configuring
|
||||||
|
// lead retrieval (licenses, custom questions), not capturing leads.
|
||||||
|
leads_loc.current.tab[exhibit_id] = 'manage';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger a reload or UI update if needed
|
|
||||||
// (The parent +page.svelte should reactively update is_signed_in)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -126,9 +126,8 @@ function fuzzy_time_ago(date_str: string) {
|
|||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
title={format_date_full(
|
title={`Added lead:\n${format_date_full(event_tracking_obj.created_on)}`}
|
||||||
event_tracking_obj.created_on
|
>
|
||||||
)}>
|
|
||||||
<Clock size="1em" />
|
<Clock size="1em" />
|
||||||
{fuzzy_time_ago(
|
{fuzzy_time_ago(
|
||||||
event_tracking_obj.created_on
|
event_tracking_obj.created_on
|
||||||
|
|||||||
@@ -62,17 +62,22 @@ let licensee_li = $derived.by(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default selection logic: Aether Admins go to "all", Licensees go to "my"
|
// Default selection: resolve to the real identity on first load.
|
||||||
|
// Also migrates any stale 'my' values persisted from older sessions.
|
||||||
|
// Identity map: shared passcode → 'shared_passcode'; licensed → their email; admin bypass → 'all'.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Wait for object to load and check if initialized
|
|
||||||
if (!exhibit_obj) return;
|
if (!exhibit_obj) return;
|
||||||
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (
|
const cur = leads_loc.current.tracking__qry__licensee_email;
|
||||||
leads_loc.current.tracking__qry__licensee_email === 'all' &&
|
// Act on 'all' (first load) OR stale 'my' (persisted from before this change)
|
||||||
!$ae_loc.administrator_access
|
if ((cur === 'all' || cur === 'my') && !$ae_loc.administrator_access) {
|
||||||
) {
|
const kv = leads_loc.current.auth_exhibit_kv?.[exhibit_id];
|
||||||
leads_loc.current.tracking__qry__licensee_email = 'my';
|
if (kv?.type === 'shared') {
|
||||||
|
leads_loc.current.tracking__qry__licensee_email = 'shared_passcode';
|
||||||
|
} else if (kv?.type === 'licensed' && kv.key) {
|
||||||
|
leads_loc.current.tracking__qry__licensee_email = kv.key;
|
||||||
|
}
|
||||||
|
// No kv entry means admin/manager bypass — leave as 'all'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -129,9 +134,9 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
|||||||
onchange={handle_search_trigger}
|
onchange={handle_search_trigger}
|
||||||
class="select select-sm max-w-fit px-1 text-xs">
|
class="select select-sm max-w-fit px-1 text-xs">
|
||||||
<option value="all">All Leads</option>
|
<option value="all">All Leads</option>
|
||||||
{#if !$ae_loc.administrator_access}
|
<!-- Shared passcode leads are always a distinct bucket — anyone who signed
|
||||||
<option value="my">My Leads</option>
|
in with the booth passcode (not a personal license) lands here. -->
|
||||||
{/if}
|
<option value="shared_passcode">Booth (Shared)</option>
|
||||||
{#each licensee_li as l (l.email)}
|
{#each licensee_li as l (l.email)}
|
||||||
<option value={l.email}>{l.full_name || l.email}</option>
|
<option value={l.email}>{l.full_name || l.email}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
|
Download,
|
||||||
Info,
|
Info,
|
||||||
Key,
|
Key,
|
||||||
Lock,
|
Lock,
|
||||||
@@ -67,9 +68,13 @@ let desc_expanded = $state(false);
|
|||||||
|
|
||||||
function handle_signout() {
|
function handle_signout() {
|
||||||
if (confirm('Sign out from this booth?')) {
|
if (confirm('Sign out from this booth?')) {
|
||||||
delete leads_loc.current.auth_exhibit_kv[exhibit_id];
|
// Use object spread instead of delete — `delete` on a PersistedState proxy
|
||||||
|
// does not reliably trigger Svelte 5 reactivity, so is_signed_in in the parent
|
||||||
|
// page would never re-evaluate and the sign-in gate would stay open.
|
||||||
|
leads_loc.current.auth_exhibit_kv = Object.fromEntries(
|
||||||
|
Object.entries(leads_loc.current.auth_exhibit_kv).filter(([k]) => k !== exhibit_id)
|
||||||
|
);
|
||||||
$events_sess.leads.entered_passcode = null;
|
$events_sess.leads.entered_passcode = null;
|
||||||
// Navigate to start tab
|
|
||||||
leads_loc.current.tab[exhibit_id] = 'start';
|
leads_loc.current.tab[exhibit_id] = 'start';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,165 +178,105 @@ function handle_signout() {
|
|||||||
exhibit_id
|
exhibit_id
|
||||||
})} />
|
})} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API / Export Access — gates the CSV export button and backend export endpoint -->
|
||||||
|
<div
|
||||||
|
class="card preset-tonal-surface flex items-center justify-between p-3">
|
||||||
|
<div class="text-[10px] font-black uppercase opacity-40">
|
||||||
|
API / Export Access
|
||||||
|
</div>
|
||||||
|
<Element_ae_obj_field_editor
|
||||||
|
object_type="event_exhibit"
|
||||||
|
object_id={exhibit_id}
|
||||||
|
field_name="leads_api_access"
|
||||||
|
field_type="checkbox"
|
||||||
|
current_value={$lq__exhibit_obj?.leads_api_access}
|
||||||
|
on_success={() =>
|
||||||
|
events_func.load_ae_obj_id__event_exhibit({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id
|
||||||
|
})}>
|
||||||
|
<div class="font-bold">
|
||||||
|
{$lq__exhibit_obj?.leads_api_access ? 'ON' : 'OFF'}
|
||||||
|
</div>
|
||||||
|
</Element_ae_obj_field_editor>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Section: Booth Profile -->
|
<!-- Section: Account Status (read-only for non-admin signed-in users) -->
|
||||||
<section class="space-y-4">
|
<!-- Admins see the editable Admin Tools section above; this is for exhibitor staff. -->
|
||||||
<div
|
{#if !$ae_loc.manager_access}
|
||||||
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
<section class="space-y-3">
|
||||||
<Store size="1.2em" class="text-primary-500" />
|
<div
|
||||||
<h3 class="text-lg font-bold tracking-wider uppercase">
|
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
||||||
Booth Profile
|
<Info size="1.2em" class="text-tertiary-500" />
|
||||||
</h3>
|
<h3 class="text-lg font-bold tracking-wider uppercase">
|
||||||
</div>
|
Account Status
|
||||||
|
</h3>
|
||||||
<div class="grid grid-cols-1 gap-6">
|
|
||||||
<!-- Name -->
|
|
||||||
<div class="card preset-tonal-surface p-4 shadow-sm">
|
|
||||||
<div class="label mb-2">
|
|
||||||
<span
|
|
||||||
class="text-xs font-black tracking-widest uppercase opacity-40"
|
|
||||||
>Exhibitor Name</span>
|
|
||||||
</div>
|
|
||||||
<Element_ae_obj_field_editor
|
|
||||||
object_type="event_exhibit"
|
|
||||||
object_id={exhibit_id}
|
|
||||||
field_name="name"
|
|
||||||
field_type="text"
|
|
||||||
current_value={$lq__exhibit_obj?.name}
|
|
||||||
display_block={true}
|
|
||||||
class_li="font-bold text-xl"
|
|
||||||
on_success={() =>
|
|
||||||
events_func.load_ae_obj_id__event_exhibit({
|
|
||||||
api_cfg: $ae_api,
|
|
||||||
exhibit_id
|
|
||||||
})} />
|
|
||||||
<p class="mt-2 text-[10px] italic opacity-50">
|
|
||||||
This name is visible to attendees when you scan their
|
|
||||||
badges.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description — collapsed by default (can be long) -->
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<div class="card preset-tonal-surface p-4 shadow-sm">
|
<!-- Payment / Priority -->
|
||||||
|
<div
|
||||||
|
class="card preset-tonal-surface flex flex-col items-center justify-center gap-1 p-3 text-center">
|
||||||
|
<div
|
||||||
|
class="text-[9px] font-black tracking-widest uppercase opacity-40">
|
||||||
|
Payment
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-sm font-bold"
|
||||||
|
class:text-success-500={$lq__exhibit_obj?.priority}
|
||||||
|
class:text-warning-500={!$lq__exhibit_obj?.priority}>
|
||||||
|
{$lq__exhibit_obj?.priority ? 'PAID' : 'PENDING'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Max Licenses -->
|
||||||
|
<div
|
||||||
|
class="card preset-tonal-surface flex flex-col items-center justify-center gap-1 p-3 text-center">
|
||||||
|
<div
|
||||||
|
class="text-[9px] font-black tracking-widest uppercase opacity-40">
|
||||||
|
Licenses
|
||||||
|
</div>
|
||||||
|
<div class="font-mono text-sm font-bold">
|
||||||
|
{$lq__exhibit_obj?.license_max ?? '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API / Export Access -->
|
||||||
|
<div
|
||||||
|
class="card preset-tonal-surface flex flex-col items-center justify-center gap-1 p-3 text-center">
|
||||||
|
<div
|
||||||
|
class="text-[9px] font-black tracking-widest uppercase opacity-40">
|
||||||
|
Export
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-sm font-bold"
|
||||||
|
class:text-success-500={$lq__exhibit_obj?.leads_api_access}
|
||||||
|
class:opacity-40={!$lq__exhibit_obj?.leads_api_access}>
|
||||||
|
{$lq__exhibit_obj?.leads_api_access ? 'ON' : 'OFF'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export button — only when API access is enabled for this exhibit -->
|
||||||
|
{#if $lq__exhibit_obj?.leads_api_access}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="focus-visible:ring-primary-500 flex w-full items-center justify-between gap-2 rounded text-left focus-visible:ring-2"
|
class="btn btn-sm preset-outlined-secondary w-full"
|
||||||
onclick={() => (desc_expanded = !desc_expanded)}
|
onclick={() =>
|
||||||
aria-expanded={desc_expanded}>
|
events_func.download_export__event_exhibit_tracking({
|
||||||
<span class="text-xs font-black tracking-widest uppercase opacity-40"
|
api_cfg: $ae_api,
|
||||||
>Booth Description / Promo</span>
|
exhibit_id,
|
||||||
<span class="shrink-0 text-xs opacity-40">
|
log_lvl: 1
|
||||||
{#if desc_expanded}
|
})}>
|
||||||
<ChevronUp size="1em" />
|
<Download size="1.2em" class="mr-2" /> Export Leads (CSV)
|
||||||
{:else}
|
|
||||||
<ChevronDown size="1em" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
{#if desc_expanded}
|
{/if}
|
||||||
<div class="mt-3">
|
</section>
|
||||||
<Element_ae_obj_field_editor
|
{/if}
|
||||||
object_type="event_exhibit"
|
|
||||||
object_id={exhibit_id}
|
|
||||||
field_name="description"
|
|
||||||
field_type="tiptap"
|
|
||||||
current_value={$lq__exhibit_obj?.description}
|
|
||||||
display_block={true}
|
|
||||||
class_li="text-sm"
|
|
||||||
on_success={() =>
|
|
||||||
events_func.load_ae_obj_id__event_exhibit({
|
|
||||||
api_cfg: $ae_api,
|
|
||||||
exhibit_id
|
|
||||||
})} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section: Staff Access -->
|
|
||||||
<section class="space-y-4">
|
|
||||||
<div
|
|
||||||
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
|
||||||
<Lock size="1.2em" class="text-warning-500" />
|
|
||||||
<h3 class="text-lg font-bold tracking-wider uppercase">
|
|
||||||
Access & Security
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<!-- Staff Passcode -->
|
|
||||||
<div class="card bg-surface-500/5 border-surface-500/10 border p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div
|
|
||||||
class="mb-1 text-[10px] font-black tracking-widest uppercase opacity-40">
|
|
||||||
Staff Passcode
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Element_ae_obj_field_editor
|
|
||||||
object_type="event_exhibit"
|
|
||||||
object_id={exhibit_id}
|
|
||||||
field_name="staff_passcode"
|
|
||||||
field_type="text"
|
|
||||||
current_value={$lq__exhibit_obj?.staff_passcode}
|
|
||||||
display_block={true}
|
|
||||||
class_li="font-mono text-xl tracking-widest font-bold"
|
|
||||||
on_success={() =>
|
|
||||||
events_func.load_ae_obj_id__event_exhibit({
|
|
||||||
api_cfg: $ae_api,
|
|
||||||
exhibit_id
|
|
||||||
})} />
|
|
||||||
</div>
|
|
||||||
<Key size="1.5em" class="opacity-20" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-[9px] italic opacity-40">
|
|
||||||
Shared code for your team to sign in to this booth.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Booth Code -->
|
|
||||||
<div class="card bg-surface-500/5 border-surface-500/10 border p-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div
|
|
||||||
class="mb-1 text-[10px] font-black tracking-widest uppercase opacity-40">
|
|
||||||
Booth Identifier
|
|
||||||
</div>
|
|
||||||
<Element_ae_obj_field_editor
|
|
||||||
object_type="event_exhibit"
|
|
||||||
object_id={exhibit_id}
|
|
||||||
field_name="code"
|
|
||||||
field_type="text"
|
|
||||||
current_value={$lq__exhibit_obj?.code}
|
|
||||||
display_block={true}
|
|
||||||
class_li="font-mono text-xl font-bold"
|
|
||||||
on_success={() =>
|
|
||||||
events_func.load_ae_obj_id__event_exhibit({
|
|
||||||
api_cfg: $ae_api,
|
|
||||||
exhibit_id
|
|
||||||
})} />
|
|
||||||
</div>
|
|
||||||
<Info size="1.5em" class="opacity-20" />
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-[9px] italic opacity-40">
|
|
||||||
Official floor plan booth number.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sign Out -->
|
|
||||||
{#if !$ae_loc.manager_access}
|
|
||||||
<button
|
|
||||||
class="btn preset-outlined-error mt-2 w-full"
|
|
||||||
onclick={handle_signout}>
|
|
||||||
<LogOut size="1.2em" class="mr-2" /> Sign Out of Booth
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section: Lead Settings -->
|
<!-- Section: Lead Settings -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
@@ -472,6 +417,161 @@ function handle_signout() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Section: Booth Profile -->
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
||||||
|
<Store size="1.2em" class="text-primary-500" />
|
||||||
|
<h3 class="text-lg font-bold tracking-wider uppercase">
|
||||||
|
Booth Profile
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-6">
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="card preset-tonal-surface p-4 shadow-sm">
|
||||||
|
<div class="label mb-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-black tracking-widest uppercase opacity-40"
|
||||||
|
>Exhibitor Name</span>
|
||||||
|
</div>
|
||||||
|
<Element_ae_obj_field_editor
|
||||||
|
object_type="event_exhibit"
|
||||||
|
object_id={exhibit_id}
|
||||||
|
field_name="name"
|
||||||
|
field_type="text"
|
||||||
|
current_value={$lq__exhibit_obj?.name}
|
||||||
|
display_block={true}
|
||||||
|
class_li="font-bold text-xl"
|
||||||
|
on_success={() =>
|
||||||
|
events_func.load_ae_obj_id__event_exhibit({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id
|
||||||
|
})} />
|
||||||
|
<p class="mt-2 text-[10px] italic opacity-50">
|
||||||
|
This name should match your booth name.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description — collapsed by default (can be long) -->
|
||||||
|
<div class="card preset-tonal-surface p-4 shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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-black tracking-widest uppercase opacity-40"
|
||||||
|
>Booth Description / Promo</span>
|
||||||
|
<span class="shrink-0 text-xs opacity-40">
|
||||||
|
{#if desc_expanded}
|
||||||
|
<ChevronUp size="1em" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown size="1em" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{#if desc_expanded}
|
||||||
|
<div class="mt-3">
|
||||||
|
<Element_ae_obj_field_editor
|
||||||
|
object_type="event_exhibit"
|
||||||
|
object_id={exhibit_id}
|
||||||
|
field_name="description"
|
||||||
|
field_type="tiptap"
|
||||||
|
current_value={$lq__exhibit_obj?.description}
|
||||||
|
display_block={true}
|
||||||
|
class_li="text-sm"
|
||||||
|
on_success={() =>
|
||||||
|
events_func.load_ae_obj_id__event_exhibit({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id
|
||||||
|
})} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section: Staff Access -->
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
||||||
|
<Lock size="1.2em" class="text-warning-500" />
|
||||||
|
<h3 class="text-lg font-bold tracking-wider uppercase">
|
||||||
|
Access & Security
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<!-- Staff Passcode -->
|
||||||
|
<div class="card bg-surface-500/5 border-surface-500/10 border p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
class="mb-1 text-[10px] font-black tracking-widest uppercase opacity-40">
|
||||||
|
Staff Passcode
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Element_ae_obj_field_editor
|
||||||
|
object_type="event_exhibit"
|
||||||
|
object_id={exhibit_id}
|
||||||
|
field_name="staff_passcode"
|
||||||
|
field_type="text"
|
||||||
|
current_value={$lq__exhibit_obj?.staff_passcode}
|
||||||
|
display_block={true}
|
||||||
|
class_li="font-mono text-xl tracking-widest font-bold"
|
||||||
|
on_success={() =>
|
||||||
|
events_func.load_ae_obj_id__event_exhibit({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id
|
||||||
|
})} />
|
||||||
|
</div>
|
||||||
|
<Key size="1.5em" class="opacity-20" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-[9px] italic opacity-40">
|
||||||
|
Shared code for your team to sign in to this booth.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Booth Code -->
|
||||||
|
<div class="card bg-surface-500/5 border-surface-500/10 border p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
class="mb-1 text-[10px] font-black tracking-widest uppercase opacity-40">
|
||||||
|
Booth Identifier
|
||||||
|
</div>
|
||||||
|
<Element_ae_obj_field_editor
|
||||||
|
object_type="event_exhibit"
|
||||||
|
object_id={exhibit_id}
|
||||||
|
field_name="code"
|
||||||
|
field_type="text"
|
||||||
|
current_value={$lq__exhibit_obj?.code}
|
||||||
|
display_block={true}
|
||||||
|
class_li="font-mono text-xl font-bold"
|
||||||
|
on_success={() =>
|
||||||
|
events_func.load_ae_obj_id__event_exhibit({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id
|
||||||
|
})} />
|
||||||
|
</div>
|
||||||
|
<Info size="1.5em" class="opacity-20" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-[9px] italic opacity-40">
|
||||||
|
Official floor plan booth number.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign Out -->
|
||||||
|
{#if !$ae_loc.manager_access}
|
||||||
|
<button
|
||||||
|
class="btn preset-outlined-error mt-2 w-full"
|
||||||
|
onclick={handle_signout}>
|
||||||
|
<LogOut size="1.2em" class="mr-2" /> Sign Out of Booth
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Section: App Settings -->
|
<!-- Section: App Settings -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import { ae_util } from '$lib/ae_utils/ae_utils';
|
|||||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||||
import { events_func } from '$lib/ae_events/ae_events_functions';
|
import { events_func } from '$lib/ae_events/ae_events_functions';
|
||||||
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 AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
|
||||||
import Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte';
|
import Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte';
|
||||||
import {
|
import {
|
||||||
Briefcase,
|
Briefcase,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Eye,
|
Eye,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
Save,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Star,
|
Star,
|
||||||
@@ -49,6 +52,36 @@ let lq__exhibit_obj = $derived(
|
|||||||
|
|
||||||
let is_edit_mode = $state(false);
|
let is_edit_mode = $state(false);
|
||||||
|
|
||||||
|
// Notes editing state
|
||||||
|
let draft_notes = $state('');
|
||||||
|
let notes_status = $state<'idle' | 'saving' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
// Keep draft in sync when viewing (not editing), so it's ready when edit mode opens.
|
||||||
|
$effect(() => {
|
||||||
|
if (!is_edit_mode) {
|
||||||
|
draft_notes = $lq__lead_obj?.exhibitor_notes ?? '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save_notes() {
|
||||||
|
if (!exhibit_tracking_id) return;
|
||||||
|
notes_status = 'saving';
|
||||||
|
try {
|
||||||
|
await events_func.update_ae_obj__exhibit_tracking({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
exhibit_id: page.params.exhibit_id ?? '',
|
||||||
|
exhibit_tracking_id,
|
||||||
|
data: { exhibitor_notes: draft_notes }
|
||||||
|
});
|
||||||
|
notes_status = 'success';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notes_status === 'success') notes_status = 'idle';
|
||||||
|
}, 2000);
|
||||||
|
} catch {
|
||||||
|
notes_status = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove / Restore flow.
|
// Remove / Restore flow.
|
||||||
// Two-click confirm for remove: idle → confirm → removing → (navigate back).
|
// Two-click confirm for remove: idle → confirm → removing → (navigate back).
|
||||||
let remove_status = $state<'idle' | 'confirm' | 'removing' | 'restoring'>(
|
let remove_status = $state<'idle' | 'confirm' | 'removing' | 'restoring'>(
|
||||||
@@ -227,18 +260,27 @@ function format_date(date: any) {
|
|||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 text-sm opacity-70">
|
class="flex items-center gap-2 text-sm opacity-70">
|
||||||
|
<a href={`mailto:${$lq__lead_obj.event_badge_email}`}
|
||||||
|
class="flex items-center gap-1 underline">
|
||||||
<Mail size="1em" class="flex-none" />
|
<Mail size="1em" class="flex-none" />
|
||||||
<span class="truncate font-mono"
|
<span class="truncate font-mono"
|
||||||
>{$lq__lead_obj.event_badge_email ||
|
>{$lq__lead_obj.event_badge_email ||
|
||||||
'No email on file'}</span>
|
'No email on file'}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 text-sm opacity-60">
|
class="flex items-center gap-2 text-sm opacity-60">
|
||||||
<CalendarDays size="1em" class="flex-none" />
|
<CalendarDays size="1em" class="flex-none" />
|
||||||
<span
|
<span
|
||||||
>Captured {format_date(
|
>Captured {ae_util.iso_datetime_formatter(
|
||||||
$lq__lead_obj.created_on
|
$lq__lead_obj.created_on, 'datetime_12_long'
|
||||||
)}</span>
|
)}</span>
|
||||||
|
{#if $lq__lead_obj.updated_on}
|
||||||
|
<span
|
||||||
|
>Updated {ae_util.iso_datetime_formatter(
|
||||||
|
$lq__lead_obj.updated_on, 'datetime_12_long'
|
||||||
|
)}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,29 +353,60 @@ function format_date(date: any) {
|
|||||||
Exhibitor Notes
|
Exhibitor Notes
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{#if is_edit_mode}
|
||||||
class="bg-surface-500/5 border-surface-500/10 min-h-[120px] rounded-xl border p-5">
|
<AE_Comp_Editor_TipTap
|
||||||
{#if is_edit_mode}
|
bind:content={draft_notes}
|
||||||
<Element_ae_obj_field_editor
|
placeholder="Add notes about this lead..." />
|
||||||
object_type="event_exhibit_tracking"
|
<div class="flex items-center justify-end gap-3">
|
||||||
object_id={exhibit_tracking_id ?? ''}
|
{#if notes_status === 'success'}
|
||||||
field_name="exhibitor_notes"
|
<span
|
||||||
field_type="tiptap"
|
class="text-success-500 flex items-center gap-1 text-sm">
|
||||||
current_value={$lq__lead_obj.exhibitor_notes}
|
<Check size="1em" /> Saved
|
||||||
object_reload={true}
|
</span>
|
||||||
display_block={true} />
|
{:else if notes_status === 'error'}
|
||||||
{:else if $lq__lead_obj.exhibitor_notes}
|
<span class="text-error-500 text-sm"
|
||||||
|
>Save failed — try again</span>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm preset-filled-primary"
|
||||||
|
disabled={notes_status === 'saving' ||
|
||||||
|
draft_notes ===
|
||||||
|
($lq__lead_obj?.exhibitor_notes ??
|
||||||
|
'')}
|
||||||
|
onclick={save_notes}>
|
||||||
|
{#if notes_status === 'saving'}
|
||||||
|
<LoaderCircle
|
||||||
|
size="1em"
|
||||||
|
class="animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Save size="1em" />
|
||||||
|
{/if}
|
||||||
|
Save Notes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if $lq__lead_obj.exhibitor_notes}
|
||||||
|
<div
|
||||||
|
class="bg-surface-500/5 border-surface-500/10 rounded-xl border p-5">
|
||||||
<div
|
<div
|
||||||
class="prose dark:prose-invert max-w-none leading-relaxed">
|
class="prose dark:prose-invert max-w-none leading-relaxed">
|
||||||
{@html $lq__lead_obj.exhibitor_notes}
|
{@html $lq__lead_obj.exhibitor_notes}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
<div
|
{:else}
|
||||||
class="flex h-full items-center justify-center text-sm italic opacity-30">
|
<div
|
||||||
No notes have been added for this lead yet.
|
class="bg-surface-500/5 border-surface-500/10 flex min-h-30 items-center justify-center rounded-xl border">
|
||||||
</div>
|
<button
|
||||||
{/if}
|
type="button"
|
||||||
</div>
|
class="btn btn-sm preset-outlined-surface"
|
||||||
|
onclick={() => {
|
||||||
|
is_edit_mode = true;
|
||||||
|
remove_status = 'idle';
|
||||||
|
}}>
|
||||||
|
<SquarePen size="1.2em" class="mr-1" /> Add Notes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user