284 lines
13 KiB
Markdown
284 lines
13 KiB
Markdown
# Aether Events — Exhibitor Leads Module (v3)
|
||
|
||
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
|
||
**Platform:** PWA only — mobile-first, offline-capable.
|
||
**Target users:** Conference exhibitors scanning attendee badges at their booths.
|
||
|
||
### Recent Changes (2026-04-03)
|
||
|
||
- Migrated Leads persisted state to Svelte‑5 PersistedState: `leads_loc` now implemented at `src/lib/stores/ae_events_stores__leads.svelte.ts` and the store version constant `AE_LEADS_LOC_VERSION` added to `src/lib/stores/store_versions.ts`.
|
||
- Payment UI adjustments: `ae_comp__exhibit_payment.svelte` now accepts a `leads_require_payment` prop and enforces the event-level `mod_exhibits_json.leads_require_payment` flag; a loading guard was added so the component waits for the exhibit record (Dexie `liveQuery`) before deciding which UI to show.
|
||
- Tests: update `tests/_helpers/leads_helpers.ts` to seed `leads_loc` defaults and `__version` when needed to avoid localStorage wipe caused by store version checks.
|
||
|
||
---
|
||
|
||
## What It Does
|
||
|
||
The Exhibitor Leads module lets conference exhibitors capture and manage attendee leads directly
|
||
from their booth. Exhibitors scan or search attendee badges and build a list of contacts they met.
|
||
All data is cached locally (IndexedDB / Dexie.js) for spotty or offline venue Wi-Fi, with
|
||
background SWR revalidation against the API when the network is available.
|
||
|
||
Key capabilities:
|
||
|
||
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
|
||
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
|
||
- **Lead detail** — custom question responses, notes (rich text), priority flag, hide/unhide
|
||
- **Export** — CSV/XLSX download of all leads for an exhibit
|
||
- **License management** — assign staff accounts (email + passcode) per max license count
|
||
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
|
||
- **Offline-first** — IndexedDB cache survives network drops; syncs on reconnect
|
||
- **PWA install** — Chrome/Android native install prompt; iOS Safari "Add to Home Screen" nudge
|
||
|
||
---
|
||
|
||
## Access Levels
|
||
|
||
Three sign-in levels are supported within this module:
|
||
|
||
| Level | How to sign in | What they can do |
|
||
|---|---|---|
|
||
| **Aether Platform Auth** | Standard Aether login (manager/trusted access) | Full admin bypass; all exhibit data |
|
||
| **Shared Exhibit Passcode** | Enter booth's `staff_passcode` | Manage licenses, view/add leads |
|
||
| **Licensed User** | Email + individual passcode from `license_li_json` | Add and manage leads for this booth |
|
||
|
||
Auth state is persisted in `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-backed).
|
||
|
||
A booth only shows in the landing page search to non-admins if it is marked `priority = true` (i.e. paid).
|
||
|
||
### `allow_tracking` Opt-In
|
||
|
||
Attendees must have `allow_tracking = true` on their badge record to be added as a lead.
|
||
Attendees without this flag are blocked at both the QR scanner and the manual search:
|
||
- QR scan shows a "Tracking Blocked" warning card (`ShieldOff` icon)
|
||
- Manual search shows an "Opt-Out" badge per result row; the "Add as Lead" button is suppressed
|
||
|
||
---
|
||
|
||
## Route Structure
|
||
|
||
```
|
||
/events/[event_id]/leads/
|
||
→ Exhibit search / landing page — find your booth
|
||
|
||
/events/[event_id]/leads/exhibit/[exhibit_id]/
|
||
→ Main exhibitor view — all 4 tabs
|
||
|
||
/events/[event_id]/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/
|
||
→ Lead detail view — edit notes, custom responses, flags
|
||
```
|
||
|
||
---
|
||
|
||
## Module Tabs
|
||
|
||
### Tab 1 — Start / Sign In
|
||
|
||
The only tab visible when not signed in as a licensed leads user.
|
||
|
||
- **Sign in with shared passcode** — grants booth management access (license management, passcode change)
|
||
- **Sign in as licensed user** — grants lead capture access (email + passcode)
|
||
- **PWA install prompt** — Chrome/Android native install button; iOS "Share → Add to Home Screen" instructions
|
||
- **License list** — shown when signed in via shared passcode or Aether admin; add/edit/remove staff slots
|
||
|
||
### Tab 2 — Add Leads
|
||
|
||
Visible only when signed in (licensed user or Aether auth).
|
||
|
||
- **Text search** — search by name, email, affiliations, badge ID
|
||
- **QR scan** — three modes (persisted per exhibit in `tab_scan_qualify`):
|
||
- **Confirm** (`rapid`) — scan, then choose per badge: **Add & Scan Next** (resets after 2s) or **Add & View Lead** (navigates to detail)
|
||
- **Auto** — no confirmation tap; adds immediately and auto-resets (high-throughput)
|
||
- **Multi** — BarcodeDetector batch scan; up to 4 badges in one frame as a confirm grid
|
||
- Previously-removed leads detected on scan — shown a "Previously Removed" card with **Restore & Scan Next** / **Restore & View Lead** buttons
|
||
- Results show "Add as Lead" or "View Lead" depending on whether already captured
|
||
- `external_person_id` and `group` resolved by auth type — see [Capture Identity](#capture-identity) below
|
||
|
||
### Tab 3 — Leads List
|
||
|
||
The main lead management view.
|
||
|
||
- **Search** — full-text across name, email, notes (local IDB fast path + API revalidation)
|
||
- **Sort** — Newest first, Oldest first, Name A→Z, Name Z→A
|
||
- **Filter by staff member** — "All Leads" or filter by individual licensed user
|
||
- **Show/hide hidden records** — toggles `hide` filter on IDB and API results
|
||
- **Export** — downloads CSV/XLSX for the exhibit (`leads_api_access` required)
|
||
|
||
### Tab 4 — Manage / Config
|
||
|
||
Exhibit configuration and app settings.
|
||
|
||
**Admin Tools** (manager_access only):
|
||
- Payment status toggle (`priority` field)
|
||
- Max licenses, small/large device counts
|
||
|
||
**Booth Profile** (all signed-in users):
|
||
- Exhibitor name, booth description (rich text)
|
||
|
||
**Access & Security**:
|
||
- View/change shared staff passcode
|
||
- Sign out button
|
||
|
||
**Lead Retrieval Config**:
|
||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode)
|
||
- Qualifiers & Questions — custom question config
|
||
- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`)
|
||
|
||
**App Settings**:
|
||
- Auto-hide header/footer toggle
|
||
- Show Extra Details toggle
|
||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
||
|
||
---
|
||
|
||
## Data Model
|
||
|
||
### `event_exhibit`
|
||
One exhibitor's presence at an event.
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `event_exhibit_id` | Primary / URL-safe ID |
|
||
| `name` | Exhibitor display name |
|
||
| `code` | Booth number |
|
||
| `staff_passcode` | Shared sign-in code |
|
||
| `priority` | `1` = paid/active |
|
||
| `license_max` | Max licensed staff slots |
|
||
| `license_li_json` | Array of `{ full_name, email, passcode }` |
|
||
| `leads_custom_questions_json` | Array of question definitions |
|
||
| `leads_device_sm_qty` / `leads_device_lg_qty` | Device count tracking |
|
||
|
||
### `event_exhibit_tracking`
|
||
One captured lead — links an exhibit to a badge.
|
||
|
||
| Field | Purpose |
|
||
|---|---|
|
||
| `event_exhibit_tracking_id` | Primary key |
|
||
| `event_exhibit_id` | Parent exhibit |
|
||
| `event_badge_id` | Captured attendee's badge |
|
||
| `external_person_id` | Capturing staff's email (from license) |
|
||
| `exhibitor_notes` | Rich text notes (HTML via TipTap) |
|
||
| `responses_json` | `{ [question_code]: { response: value } }` |
|
||
| `priority` | Star/flag for high-priority leads |
|
||
| `hide` | Soft-delete / hide from list |
|
||
| Denormalized badge fields | `event_badge_full_name`, `event_badge_email`, `event_badge_affiliations`, `event_badge_professional_title` |
|
||
|
||
---
|
||
|
||
## Key Files
|
||
|
||
### Routes
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `leads/+page.svelte` | Exhibit search/landing |
|
||
| `leads/exhibit/[exhibit_id]/+page.svelte` | Main exhibitor view — orchestrates all tabs |
|
||
| `leads/exhibit/[exhibit_id]/+layout.svelte` / `+layout.ts` | Layout / data load |
|
||
| `leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte` | Lead detail |
|
||
|
||
### Components
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `ae_tab__start.svelte` | Tab 1 — welcome, sign-in, license list |
|
||
| `ae_tab__add.svelte` | Tab 2 — QR scan + text search toggle |
|
||
| `ae_tab__manage.svelte` | Tab 4 — admin tools, booth config, app settings |
|
||
| `ae_comp__exhibit_signin.svelte` | Sign-in UI (shared passcode + licensed user) |
|
||
| `ae_comp__lead_qr_scanner.svelte` | QR scanner (rapid / qualify mode) |
|
||
| `ae_comp__lead_manual_search.svelte` | Manual badge search + add |
|
||
| `ae_comp__exhibit_tracking_search.svelte` | Lead list search/filter/sort bar |
|
||
| `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer |
|
||
| `ae_comp__exhibit_license_list.svelte` | License slot manager |
|
||
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor |
|
||
| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder |
|
||
| `ae_comp__exhibit_search.svelte` | Exhibit search on the landing page |
|
||
| `lead/ae_comp__lead_detail_form.svelte` | Custom question response editor |
|
||
|
||
### Lib Functions
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `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 |
|
||
|
||
Both aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.
|
||
|
||
---
|
||
|
||
## Offline / PWA Notes
|
||
|
||
- All data is stored in `db_events` (Dexie.js) — `exhibit` and `exhibit_tracking` tables
|
||
- SWR pattern: IDB cache returned immediately; background API fetch updates IDB and triggers UI refresh
|
||
- Search: local IDB first pass (fast), then API revalidation via `search__exhibit_tracking`
|
||
- `beforeinstallprompt` event captured at module load time (`src/lib/pwa/pwa_install.svelte.ts`)
|
||
— fires within ~1 second of page load, before any Svelte `$effect` runs
|
||
- iOS Safari: no native install prompt; shows "Share → Add to Home Screen" instructions instead
|
||
|
||
---
|
||
|
||
## Capture Identity
|
||
|
||
`external_person_id` and `group` on every `event_exhibit_tracking` record record who captured the lead. Resolved at capture time in all three lead capture components (single scanner, multi scanner, manual search):
|
||
|
||
| Auth type | `kv.type` | Value stored |
|
||
| --- | --- | --- |
|
||
| Licensed exhibit user | `'licensed'` | Their email address (`kv.key`) |
|
||
| Shared exhibit passcode | `'shared'` | `'shared_passcode'` (label — raw passcode is NOT stored) |
|
||
| Aether user (admin bypass, no kv) | `undefined` | `$ae_loc.access_type` — e.g. `'trusted'`, `'manager'`, `'super'` |
|
||
|
||
`kv` = `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-persisted exhibit sign-in state).
|
||
|
||
---
|
||
|
||
## Lead Soft-Delete / Re-enable
|
||
|
||
Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors:
|
||
|
||
- **Leads list** always filters out `enable = false` records (both IDB fast-path and API results) — no flash of removed records
|
||
- **QR scanner**: if a previously-removed badge is scanned, the scanner detects it via `existing_leads_map` (IDB) or API fallback search (`search__exhibit_tracking` with `qry_badge_id` + `enabled: 'not_enabled'`) and shows the reenable card instead of an error
|
||
- **Lead detail page**: "Remove Lead" button (two-click confirm in header) sets `enable = false` and navigates back. "Restore Lead" card appears at the bottom of the right sidebar when `enable` is falsy.
|
||
- `search__exhibit_tracking` supports `qry_badge_id` param (added) and `enabled: 'not_enabled'` to find disabled records for a specific badge + exhibit combination
|
||
|
||
---
|
||
|
||
## Known Gaps
|
||
|
||
None currently. See TODO__Agents.md for remaining smoke test items.
|
||
|
||
## Implemented (previously listed as gaps)
|
||
|
||
### Payment / Stripe
|
||
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||
confirmation card), Stripe not configured (admin hint), payment form with license tier selector.
|
||
Visibility is event-wide: set `event.mod_exhibits_json.leads_require_payment = true` in the event
|
||
settings JSON to enable. When `false` (default), both the header CreditCard button and the
|
||
"Licenses & Billing" accordion in the Manage tab are hidden. The Stripe component itself is
|
||
unchanged — gating is done in `+page.svelte` and `ae_tab__manage.svelte`.
|
||
|
||
### License Management — Shared Passcode Access
|
||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
||
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
|
||
|
||
### "My Leads" filter for shared-passcode users
|
||
Fixed. `external_person_id` is stored as the literal `'shared_passcode'` for shared users (not
|
||
the raw passcode string). The `search_params` derived in `+page.svelte` now checks `kv.type ===
|
||
'shared'` and resolves to `'shared_passcode'` instead of `kv.key`, so the "My Leads" filter
|
||
correctly returns their captured records.
|
||
|
||
---
|
||
|
||
## OSIT Admin Notes
|
||
|
||
- Mark `priority = 1` on an exhibit to make it visible in public search and to enable lead capture
|
||
- `license_max` controls how many licensed staff slots an exhibit can have
|
||
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
|
||
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
|
||
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id]/`
|
||
|
||
|
||
## Old Files for Reference
|
||
|
||
@backups/legacy/events_leads_v2/exhibit/[slug]/+page.svelte
|
||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_manage.svelte
|
||
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_payment.svelte |