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:
Scott Idem
2026-04-06 19:25:38 -04:00
parent 10e9206ca4
commit 50e83502ff
7 changed files with 374 additions and 202 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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>