- Add MODULE__AE_Events_Exhibitor_Leads.md — full module reference (auth model, tabs, data model, routes, offline/PWA notes, OSIT admin notes) - Add MODULE__AE_Events_Badges_Onsite.md — onsite printing guide (browser settings, CUPS/Linux setup, Zebra ZC10L section with test results, Epson stub, troubleshooting) - Archive PROJECT__AE_Events_Exhibitor_Leads_v3*.md + Zebra test day doc → history/ - Fix leads license management access: ae_tab__manage.svelte license section now visible to administrator_access OR shared-passcode sign-in (type === 'shared'); matches spec - Add type field to LeadsLocState.auth_exhibit_kv interface (was set at runtime but missing from type) - Silence QR code console noise: entry log gated at log_lvl >= 2, data URL log removed - vite.config.ts: exclude documentation/ and tests/ from Vite HMR file watcher Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
236 lines
9.7 KiB
Markdown
236 lines
9.7 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.
|
||
|
||
---
|
||
|
||
## 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** — two modes:
|
||
- **Rapid** — auto-resets after 2 seconds for the next scan (high volume)
|
||
- **Qualify** — navigates immediately to lead detail to capture notes per scan
|
||
- Results show "Add as Lead" or "View Lead" depending on whether already captured
|
||
- All new leads are linked to the capturing user's email (`external_person_id`)
|
||
|
||
### 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` only; gap: should also allow shared-passcode users — see Known Gaps)
|
||
- Qualifiers & Questions — custom question config
|
||
- Licenses & Billing — stub (Stripe not yet implemented)
|
||
|
||
**App Settings**:
|
||
- Auto-hide header/footer toggle
|
||
- Show Payment Tab 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` / `event_exhibit_id_random` | 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
|
||
|
||
---
|
||
|
||
## Known Gaps
|
||
|
||
### Payment / Stripe
|
||
`ae_comp__exhibit_payment.svelte` is a stub. The Stripe integration is not implemented.
|
||
The payment tab can be hidden via "Show Payment Tab" in App Settings.
|
||
|
||
### 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'`).
|
||
Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte):
|
||
```svelte
|
||
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
|
||
```
|
||
|
||
---
|
||
|
||
## 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_random]/`
|