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
|
||||
? 'variant-filled-success'
|
||||
: 'variant-soft-surface'}">
|
||||
{display_value ? 'Enabled' : 'Disabled'}
|
||||
{display_value ? 'True' : 'False'}
|
||||
</span>
|
||||
{:else if field_type === 'tiptap'}
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
@@ -260,7 +260,7 @@ function toggle_edit() {
|
||||
type="checkbox"
|
||||
bind:checked={draft_value}
|
||||
class="checkbox" />
|
||||
<span>{draft_value ? 'Enabled' : 'Disabled'}</span>
|
||||
<span>{draft_value ? 'True' : 'False'}</span>
|
||||
</label>
|
||||
{:else if field_type === 'tiptap'}
|
||||
<AE_Comp_Editor_TipTap
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import {
|
||||
events_sess,
|
||||
events_slct
|
||||
} from '$lib/stores/ae_events_stores';
|
||||
import { events_sess } from '$lib/stores/ae_events_stores';
|
||||
import { leads_loc } from '$lib/stores/ae_events_stores__leads.svelte';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { page } from '$app/state';
|
||||
@@ -14,7 +11,6 @@ import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import {
|
||||
CreditCard,
|
||||
Download,
|
||||
LayoutGrid,
|
||||
List as ListIcon,
|
||||
LoaderCircle,
|
||||
Plus,
|
||||
@@ -148,9 +144,9 @@ let stripe_cfg = $derived({
|
||||
let search_params = $derived.by(() => {
|
||||
let licensee_email = leads_loc.current.tracking__qry__licensee_email;
|
||||
|
||||
// Resolve "My Leads" to the correct identity used when storing leads.
|
||||
// Shared-passcode users store 'shared_passcode' literal (not the passcode string itself).
|
||||
// Licensed users store their email. Aether bypass users store $ae_loc.access_type.
|
||||
// 'my' is a legacy value — the search component now resolves to the real identity
|
||||
// on mount and migrates stale persisted values. This block is a silent fallback
|
||||
// for any session that still has 'my' before the component has had a chance to run.
|
||||
if (licensee_email === 'my') {
|
||||
const kv = leads_loc.current.auth_exhibit_kv?.[page.params.exhibit_id ?? ''];
|
||||
licensee_email = kv?.type === 'shared'
|
||||
|
||||
@@ -108,13 +108,12 @@ function complete_signin(key: string, type: string) {
|
||||
updated_on: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Also update session passcode if shared mode
|
||||
if (type === 'shared') {
|
||||
$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>
|
||||
|
||||
|
||||
@@ -126,9 +126,8 @@ function fuzzy_time_ago(date_str: string) {
|
||||
{/if}
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
title={format_date_full(
|
||||
event_tracking_obj.created_on
|
||||
)}>
|
||||
title={`Added lead:\n${format_date_full(event_tracking_obj.created_on)}`}
|
||||
>
|
||||
<Clock size="1em" />
|
||||
{fuzzy_time_ago(
|
||||
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(() => {
|
||||
// Wait for object to load and check if initialized
|
||||
if (!exhibit_obj) return;
|
||||
|
||||
untrack(() => {
|
||||
if (
|
||||
leads_loc.current.tracking__qry__licensee_email === 'all' &&
|
||||
!$ae_loc.administrator_access
|
||||
) {
|
||||
leads_loc.current.tracking__qry__licensee_email = 'my';
|
||||
const cur = leads_loc.current.tracking__qry__licensee_email;
|
||||
// Act on 'all' (first load) OR stale 'my' (persisted from before this change)
|
||||
if ((cur === 'all' || cur === 'my') && !$ae_loc.administrator_access) {
|
||||
const kv = leads_loc.current.auth_exhibit_kv?.[exhibit_id];
|
||||
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}
|
||||
class="select select-sm max-w-fit px-1 text-xs">
|
||||
<option value="all">All Leads</option>
|
||||
{#if !$ae_loc.administrator_access}
|
||||
<option value="my">My Leads</option>
|
||||
{/if}
|
||||
<!-- Shared passcode leads are always a distinct bucket — anyone who signed
|
||||
in with the booth passcode (not a personal license) lands here. -->
|
||||
<option value="shared_passcode">Booth (Shared)</option>
|
||||
{#each licensee_li as l (l.email)}
|
||||
<option value={l.email}>{l.full_name || l.email}</option>
|
||||
{/each}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Clock,
|
||||
CreditCard,
|
||||
Database,
|
||||
Download,
|
||||
Info,
|
||||
Key,
|
||||
Lock,
|
||||
@@ -67,9 +68,13 @@ let desc_expanded = $state(false);
|
||||
|
||||
function handle_signout() {
|
||||
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;
|
||||
// Navigate to start tab
|
||||
leads_loc.current.tab[exhibit_id] = 'start';
|
||||
}
|
||||
}
|
||||
@@ -173,165 +178,105 @@ function handle_signout() {
|
||||
exhibit_id
|
||||
})} />
|
||||
</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>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- 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 is visible to attendees when you scan their
|
||||
badges.
|
||||
</p>
|
||||
<!-- Section: Account Status (read-only for non-admin signed-in users) -->
|
||||
<!-- Admins see the editable Admin Tools section above; this is for exhibitor staff. -->
|
||||
{#if !$ae_loc.manager_access}
|
||||
<section class="space-y-3">
|
||||
<div
|
||||
class="border-surface-500/10 flex items-center gap-2 border-b pb-2">
|
||||
<Info size="1.2em" class="text-tertiary-500" />
|
||||
<h3 class="text-lg font-bold tracking-wider uppercase">
|
||||
Account Status
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Description — collapsed by default (can be long) -->
|
||||
<div class="card preset-tonal-surface p-4 shadow-sm">
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<!-- 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
|
||||
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>
|
||||
class="btn btn-sm preset-outlined-secondary w-full"
|
||||
onclick={() =>
|
||||
events_func.download_export__event_exhibit_tracking({
|
||||
api_cfg: $ae_api,
|
||||
exhibit_id,
|
||||
log_lvl: 1
|
||||
})}>
|
||||
<Download size="1.2em" class="mr-2" /> Export Leads (CSV)
|
||||
</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>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Section: Lead Settings -->
|
||||
<section class="space-y-4">
|
||||
@@ -472,6 +417,161 @@ function handle_signout() {
|
||||
</div>
|
||||
</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 class="space-y-4">
|
||||
<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 { 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 AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
|
||||
import Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte';
|
||||
import {
|
||||
Briefcase,
|
||||
CalendarDays,
|
||||
Check,
|
||||
ChevronLeft,
|
||||
Eye,
|
||||
FileText,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
Mail,
|
||||
MapPin,
|
||||
RotateCcw,
|
||||
Save,
|
||||
ShieldCheck,
|
||||
SquarePen,
|
||||
Star,
|
||||
@@ -49,6 +52,36 @@ let lq__exhibit_obj = $derived(
|
||||
|
||||
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.
|
||||
// Two-click confirm for remove: idle → confirm → removing → (navigate back).
|
||||
let remove_status = $state<'idle' | 'confirm' | 'removing' | 'restoring'>(
|
||||
@@ -227,18 +260,27 @@ function format_date(date: any) {
|
||||
{/if}
|
||||
<div
|
||||
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" />
|
||||
<span class="truncate font-mono"
|
||||
>{$lq__lead_obj.event_badge_email ||
|
||||
'No email on file'}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 text-sm opacity-60">
|
||||
<CalendarDays size="1em" class="flex-none" />
|
||||
<span
|
||||
>Captured {format_date(
|
||||
$lq__lead_obj.created_on
|
||||
>Captured {ae_util.iso_datetime_formatter(
|
||||
$lq__lead_obj.created_on, 'datetime_12_long'
|
||||
)}</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>
|
||||
@@ -311,29 +353,60 @@ function format_date(date: any) {
|
||||
Exhibitor Notes
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-500/5 border-surface-500/10 min-h-[120px] rounded-xl border p-5">
|
||||
{#if is_edit_mode}
|
||||
<Element_ae_obj_field_editor
|
||||
object_type="event_exhibit_tracking"
|
||||
object_id={exhibit_tracking_id ?? ''}
|
||||
field_name="exhibitor_notes"
|
||||
field_type="tiptap"
|
||||
current_value={$lq__lead_obj.exhibitor_notes}
|
||||
object_reload={true}
|
||||
display_block={true} />
|
||||
{:else if $lq__lead_obj.exhibitor_notes}
|
||||
{#if is_edit_mode}
|
||||
<AE_Comp_Editor_TipTap
|
||||
bind:content={draft_notes}
|
||||
placeholder="Add notes about this lead..." />
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
{#if notes_status === 'success'}
|
||||
<span
|
||||
class="text-success-500 flex items-center gap-1 text-sm">
|
||||
<Check size="1em" /> Saved
|
||||
</span>
|
||||
{:else if notes_status === 'error'}
|
||||
<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
|
||||
class="prose dark:prose-invert max-w-none leading-relaxed">
|
||||
{@html $lq__lead_obj.exhibitor_notes}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-full items-center justify-center text-sm italic opacity-30">
|
||||
No notes have been added for this lead yet.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-surface-500/5 border-surface-500/10 flex min-h-30 items-center justify-center rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user