From 50e83502ffc3f98887f31bfbea52c9d001bbf980 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 6 Apr 2026 19:25:38 -0400 Subject: [PATCH] =?UTF-8?q?leads:=20UX=20improvements=20=E2=80=94=20manage?= =?UTF-8?q?=20tab,=20sign-in=20flow,=20notes=20editor,=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../element_ae_obj_field_editor.svelte | 4 +- .../leads/exhibit/[exhibit_id]/+page.svelte | 14 +- .../ae_comp__exhibit_signin.svelte | 7 +- .../ae_comp__exhibit_tracking_obj_li.svelte | 5 +- .../ae_comp__exhibit_tracking_search.svelte | 27 +- .../[exhibit_id]/ae_tab__manage.svelte | 404 +++++++++++------- .../lead/[exhibit_tracking_id]/+page.svelte | 115 ++++- 7 files changed, 374 insertions(+), 202 deletions(-) diff --git a/src/lib/elements/element_ae_obj_field_editor.svelte b/src/lib/elements/element_ae_obj_field_editor.svelte index 1ef1cdf5..78c32ca0 100644 --- a/src/lib/elements/element_ae_obj_field_editor.svelte +++ b/src/lib/elements/element_ae_obj_field_editor.svelte @@ -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'} {:else if field_type === 'tiptap'}
@@ -260,7 +260,7 @@ function toggle_edit() { type="checkbox" bind:checked={draft_value} class="checkbox" /> - {draft_value ? 'Enabled' : 'Disabled'} + {draft_value ? 'True' : 'False'} {:else if field_type === 'tiptap'} -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' diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte index e70bbf9c..4ebb9107 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte @@ -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) } diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_obj_li.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_obj_li.svelte index 6cac86bd..2955c4e6 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_obj_li.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_obj_li.svelte @@ -126,9 +126,8 @@ function fuzzy_time_ago(date_str: string) { {/if}
+ title={`Added lead:\n${format_date_full(event_tracking_obj.created_on)}`} + > {fuzzy_time_ago( event_tracking_obj.created_on diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_search.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_search.svelte index b27af1cf..5f015668 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_search.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_tracking_search.svelte @@ -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(fn: (event: T) => void) { onchange={handle_search_trigger} class="select select-sm max-w-fit px-1 text-xs"> - {#if !$ae_loc.administrator_access} - - {/if} + + {#each licensee_li as l (l.email)} {/each} diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte index 1b038944..9213dab5 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte @@ -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 })} />
+ + +
+
+ API / Export Access +
+ + events_func.load_ae_obj_id__event_exhibit({ + api_cfg: $ae_api, + exhibit_id + })}> +
+ {$lq__exhibit_obj?.leads_api_access ? 'ON' : 'OFF'} +
+
+
{/if} - -
-
- -

- Booth Profile -

-
- -
- -
-
- Exhibitor Name -
- - events_func.load_ae_obj_id__event_exhibit({ - api_cfg: $ae_api, - exhibit_id - })} /> -

- This name is visible to attendees when you scan their - badges. -

+ + + {#if !$ae_loc.manager_access} +
+
+ +

+ Account Status +

- -
+
+ +
+
+ Payment +
+
+ {$lq__exhibit_obj?.priority ? 'PAID' : 'PENDING'} +
+
+ + +
+
+ Licenses +
+
+ {$lq__exhibit_obj?.license_max ?? '—'} +
+
+ + +
+
+ Export +
+
+ {$lq__exhibit_obj?.leads_api_access ? 'ON' : 'OFF'} +
+
+
+ + + {#if $lq__exhibit_obj?.leads_api_access} - {#if desc_expanded} -
- - events_func.load_ae_obj_id__event_exhibit({ - api_cfg: $ae_api, - exhibit_id - })} /> -
- {/if} -
-
-
- - -
-
- -

- Access & Security -

-
- -
- -
-
-
-
- Staff Passcode -
- - - events_func.load_ae_obj_id__event_exhibit({ - api_cfg: $ae_api, - exhibit_id - })} /> -
- -
-

- Shared code for your team to sign in to this booth. -

-
- - -
-
-
-
- Booth Identifier -
- - events_func.load_ae_obj_id__event_exhibit({ - api_cfg: $ae_api, - exhibit_id - })} /> -
- -
-

- Official floor plan booth number. -

-
-
- - - {#if !$ae_loc.manager_access} - - {/if} -
+ {/if} + + {/if}
@@ -472,6 +417,161 @@ function handle_signout() {
+ +
+
+ +

+ Booth Profile +

+
+ +
+ +
+
+ Exhibitor Name +
+ + events_func.load_ae_obj_id__event_exhibit({ + api_cfg: $ae_api, + exhibit_id + })} /> +

+ This name should match your booth name. +

+
+ + +
+ + {#if desc_expanded} +
+ + events_func.load_ae_obj_id__event_exhibit({ + api_cfg: $ae_api, + exhibit_id + })} /> +
+ {/if} +
+
+
+ + +
+
+ +

+ Access & Security +

+
+ +
+ +
+
+
+
+ Staff Passcode +
+ + + events_func.load_ae_obj_id__event_exhibit({ + api_cfg: $ae_api, + exhibit_id + })} /> +
+ +
+

+ Shared code for your team to sign in to this booth. +

+
+ + +
+
+
+
+ Booth Identifier +
+ + events_func.load_ae_obj_id__event_exhibit({ + api_cfg: $ae_api, + exhibit_id + })} /> +
+ +
+

+ Official floor plan booth number. +

+
+
+ + + {#if !$ae_loc.manager_access} + + {/if} +
+
('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}
Captured {format_date( - $lq__lead_obj.created_on + >Captured {ae_util.iso_datetime_formatter( + $lq__lead_obj.created_on, 'datetime_12_long' )} + {#if $lq__lead_obj.updated_on} + Updated {ae_util.iso_datetime_formatter( + $lq__lead_obj.updated_on, 'datetime_12_long' + )} + {/if}
@@ -311,29 +353,60 @@ function format_date(date: any) { Exhibitor Notes -
- {#if is_edit_mode} - - {:else if $lq__lead_obj.exhibitor_notes} + {#if is_edit_mode} + +
+ {#if notes_status === 'success'} + + Saved + + {:else if notes_status === 'error'} + Save failed — try again + {/if} + +
+ {:else if $lq__lead_obj.exhibitor_notes} +
{@html $lq__lead_obj.exhibitor_notes}
- {:else} -
- No notes have been added for this lead yet. -
- {/if} -
+
+ {:else} +
+ +
+ {/if}