feat: leads — add show/hide hidden records toggle + update stale README gaps

- Add Eye/EyeOff toggle to exhibit tracking search bar; wired to
  existing $events_loc.leads.show_hidden store field
- Guard + init the show_hidden field in +page.svelte
- Add show_hidden to search_params so toggling triggers a refresh
- Apply hide filter in local IDB search path (skip hidden unless toggled)
- Pass hidden: 'not_hidden' | 'all' to API search__exhibit_tracking call
- Apply hide filter in filtered_lead_li for the broad liveQuery fallback path
- README: remove stale allow_tracking/PWA/export gaps; add Implemented section;
  fix ae_events_functions import path (moved to ae_events/ subdir)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-20 11:09:16 -04:00
parent 2c570c82dc
commit 9a43879535
3 changed files with 51 additions and 11 deletions

View File

@@ -109,13 +109,22 @@ Two scan modes (toggled per exhibit):
## Known Gaps / Not Yet Implemented ## Known Gaps / Not Yet Implemented
- **`allow_tracking` gate** — spec says badge must have `allow_tracking = true` before a lead can
be added. Neither the QR scanner nor manual search enforce this yet.
- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is a stub. The payment tab can be - **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is a stub. The payment tab can be
hidden via the "Show Payment Tab" toggle in the Manage tab's App Settings. hidden via the "Show Payment Tab" toggle in the Manage tab's App Settings.
- **Export endpoint** — `download_export__event_exhibit_tracking` calls a legacy V1 endpoint - **License management access for shared-passcode users** — spec says booth staff signed in via
(`/event/exhibit/{id}/tracking/export`). Verify it's live on the backend before demoing export. the shared passcode should be able to manage licenses. Currently gated behind
- **PWA install prompt** — spec calls for nudging exhibitors to install as a PWA on the Start tab. `$ae_loc.administrator_access` only (in `ae_tab__manage.svelte`).
## Implemented (previously listed as gaps)
- **`allow_tracking` gate** — enforced in both `ae_comp__lead_qr_scanner.svelte` and
`ae_comp__lead_manual_search.svelte`. Badges without `allow_tracking = true` are blocked.
- **Show/hide hidden records** — toggle in `ae_comp__exhibit_tracking_search.svelte`; filters
local IDB search, API search (`hidden` param), and `filtered_lead_li` in `+page.svelte`.
- **Export endpoint** — `download_export__event_exhibit_tracking` uses V3 action endpoint
`/v3/action/event_exhibit/{id}/tracking_export`. Gated on `leads_api_access === true`.
- **PWA install prompt** — `element_pwa_install_prompt.svelte` placed on the Start tab.
Handles Chrome/Android native install and iOS Safari manual instructions.
--- ---
@@ -123,4 +132,4 @@ Two scan modes (toggled per exhibit):
`src/lib/ae_events/ae_events__exhibit.ts` — exhibit load, search, create, update `src/lib/ae_events/ae_events__exhibit.ts` — exhibit load, search, create, update
`src/lib/ae_events/ae_events__exhibit_tracking.ts` — tracking load, search, create, update, export `src/lib/ae_events/ae_events__exhibit_tracking.ts` — tracking load, search, create, update, export
Both are aggregated into `events_func` via `src/lib/ae_events_functions.ts`. Both are aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.

View File

@@ -33,6 +33,8 @@
$events_loc.leads.tracking__qry__sort_order = 'created_desc'; $events_loc.leads.tracking__qry__sort_order = 'created_desc';
if (typeof $events_loc.leads.refresh_interval_sec === 'undefined') if (typeof $events_loc.leads.refresh_interval_sec === 'undefined')
$events_loc.leads.refresh_interval_sec = 25; $events_loc.leads.refresh_interval_sec = 25;
if (typeof $events_loc.leads.show_hidden === 'undefined')
$events_loc.leads.show_hidden = false;
} }
// --- Sign-In State (Derived) --- // --- Sign-In State (Derived) ---
@@ -74,9 +76,12 @@
// (API or IDB), it MUST match the selected licensee. // (API or IDB), it MUST match the selected licensee.
let filtered_lead_li = $derived.by(() => { let filtered_lead_li = $derived.by(() => {
const licensee_filter = search_params.licensee_email; const licensee_filter = search_params.licensee_email;
if (licensee_filter === 'all') return raw_lead_li; const show_hidden = search_params.show_hidden;
return raw_lead_li.filter(lead => { return raw_lead_li.filter(lead => {
// Exclude hidden leads unless show_hidden is toggled on
if (!show_hidden && lead.hide) return false;
if (licensee_filter === 'all') return true;
const capturer = lead.external_person_id || lead.group; const capturer = lead.external_person_id || lead.group;
return capturer === licensee_filter; return capturer === licensee_filter;
}); });
@@ -138,7 +143,8 @@
sort: $events_loc.leads.tracking__qry__sort_order, sort: $events_loc.leads.tracking__qry__sort_order,
licensee_email: licensee_email, licensee_email: licensee_email,
exhibit_id: page.params.exhibit_id, exhibit_id: page.params.exhibit_id,
remote_first: $events_loc.leads.tracking__qry__remote_first remote_first: $events_loc.leads.tracking__qry__remote_first,
show_hidden: $events_loc.leads.show_hidden ?? false
}; };
}); });
@@ -205,7 +211,10 @@
.where('event_exhibit_id') .where('event_exhibit_id')
.equals(target_exhibit_id) .equals(target_exhibit_id)
.filter((tracking) => { .filter((tracking) => {
// 1. Licensee Email Filter // 1. Hide filter — exclude hidden records unless show_hidden is on
if (!params.show_hidden && tracking.hide) return false;
// 2. Licensee Email Filter
if (target_licensee_email !== 'all') { if (target_licensee_email !== 'all') {
if (tracking.external_person_id !== target_licensee_email) return false; if (tracking.external_person_id !== target_licensee_email) return false;
} }
@@ -313,6 +322,7 @@
event_exhibit_id: q_exhibit_id, event_exhibit_id: q_exhibit_id,
fulltext_search_qry_str: qry_str || null, fulltext_search_qry_str: qry_str || null,
qry_external_person_id: q_licensee_email, qry_external_person_id: q_licensee_email,
hidden: params.show_hidden ? 'all' : 'not_hidden',
order_by_li, order_by_li,
limit: 150 limit: 150
}); });

View File

@@ -7,7 +7,7 @@
let { exhibit_id, log_lvl = 0 }: Props = $props(); let { exhibit_id, log_lvl = 0 }: Props = $props();
// *** Import other supporting libraries // *** Import other supporting libraries
import { Library, LoaderCircle, RemoveFormatting, Search } from '@lucide/svelte'; import { Eye, EyeOff, Library, LoaderCircle, RemoveFormatting, Search } from '@lucide/svelte';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores'; import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
@@ -165,6 +165,27 @@
<div <div
class="flex flex-row flex-wrap items-center justify-center gap-2 opacity-70 hover:opacity-100 transition-all" class="flex flex-row flex-wrap items-center justify-center gap-2 opacity-70 hover:opacity-100 transition-all"
> >
<!-- Show/Hide hidden records toggle — always visible -->
<button
type="button"
class="flex items-center gap-1 cursor-pointer px-2 py-1 rounded-token text-xs font-semibold transition-colors"
class:preset-tonal-warning={$events_loc.leads.show_hidden}
class:preset-tonal-surface={!$events_loc.leads.show_hidden}
onclick={() => {
$events_loc.leads.show_hidden = !$events_loc.leads.show_hidden;
handle_search_trigger();
}}
title={$events_loc.leads.show_hidden ? 'Showing hidden leads — click to hide them' : 'Hidden leads excluded — click to show all'}
>
{#if $events_loc.leads.show_hidden}
<Eye size="1em" />
<span>Showing Hidden</span>
{:else}
<EyeOff size="1em" />
<span>Hidden Excluded</span>
{/if}
</button>
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<label <label
class="flex items-center gap-1 cursor-pointer bg-surface-200-800 px-2 py-1 rounded-token text-xs font-semibold" class="flex items-center gap-1 cursor-pointer bg-surface-200-800 px-2 py-1 rounded-token text-xs font-semibold"