diff --git a/documentation/MODULE__AE_Events_Exhibitor_Leads.md b/documentation/MODULE__AE_Events_Exhibitor_Leads.md index f8d81239..150dc3ad 100644 --- a/documentation/MODULE__AE_Events_Exhibitor_Leads.md +++ b/documentation/MODULE__AE_Events_Exhibitor_Leads.md @@ -114,13 +114,12 @@ Exhibit configuration and app settings. - 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) +- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode) - Qualifiers & Questions — custom question config -- Licenses & Billing — stub (Stripe not yet implemented) +- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`) **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) @@ -239,17 +238,27 @@ Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors ## 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 a stub. The Stripe integration is not implemented. -The payment tab can be hidden via "Show Payment Tab" in App Settings. +`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'`). -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'} -``` + +### "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. --- diff --git a/src/routes/events/[event_id]/(leads)/README.md b/src/routes/events/[event_id]/(leads)/README.md index a54b1f7a..d94cbbba 100644 --- a/src/routes/events/[event_id]/(leads)/README.md +++ b/src/routes/events/[event_id]/(leads)/README.md @@ -55,7 +55,7 @@ All data is cached in IndexedDB (Dexie.js) for offline use, with background API | `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer | | `ae_comp__exhibit_license_list.svelte` | License slot manager (admin) | | `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor (admin) | -| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder, not functional | +| `ae_comp__exhibit_payment.svelte` | Stripe payment component (fully implemented) | | `ae_comp__exhibit_search.svelte` | Exhibit search input on the landing page | ### Lead detail components (within `lead/[exhibit_tracking_id]/`) @@ -114,11 +114,16 @@ Two scan modes (toggled per exhibit): ## Known Gaps / Not Yet Implemented -- **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. +- None currently. See `TODO__Agents.md` for the remaining smoke test checklist. ## Implemented (previously listed as gaps) +- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is fully implemented. Three states: + paid (`priority=true` green card), Stripe not configured (admin hint), payment form. + Visibility is controlled event-wide by `event.mod_exhibits_json.leads_require_payment`. + Set to `true` in event settings JSON to enable Stripe for an event; default `false` hides + all billing UI (header CreditCard button + Manage tab "Licenses & Billing" accordion). + - **`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 diff --git a/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte b/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte new file mode 100644 index 00000000..7ecb840b --- /dev/null +++ b/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte @@ -0,0 +1,317 @@ + + + + Leads Config + + +{#if !$ae_loc.administrator_access} +
+ +

Administrator access required.

+
+{:else} +
+ + +
+
+ + + + +

Leads Config

+
+
+ {#if save_status === 'success'} + + Saved + + {:else if save_status === 'error'} + + Error saving + + {/if} + +
+
+ +

+ Changes here update event.mod_exhibits_json and take effect immediately + for all exhibitors at this event (no re-login required). +

+ + {#if !draft_initialized} +

Loading event config...

+ {:else} + + + + +
+ + {#if sections.payment} +
+ +
+ + + The Stripe payment component code is always present. This toggle only + controls visibility — no code changes needed when switching events. + +
+
+ {/if} +
+ + + + +
+ + {#if sections.stripe} +
+
+ + + These keys apply only to this event's Exhibitor Leads module. + Find them in the Stripe Dashboard → Products → Buy Buttons. + The Buy Button IDs are per license tier (1, 3, 6, or 10 users). + +
+ + + +
+ + + + +
+
+ {/if} +
+ + +
+ +
+ + {/if} +
+{/if} diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte index 60b6f128..4a453527 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte @@ -134,15 +134,41 @@ const lq__exhibit_obj = liveQuery(() => { return db_events.exhibit.get(exhibit_id); }); +// Event-level config — read mod_exhibits_json for payment visibility + Stripe keys. +// Configured via /events/[event_id]/leads/config (administrator only). +const lq__event_obj = liveQuery(() => { + const event_id = page.params.event_id; + if (!event_id) return undefined; + return db_events.event.get(event_id); +}); + +// When false (default), all billing UI is hidden — set true when event uses Stripe payment. +let leads_require_payment = $derived( + ($lq__event_obj?.mod_exhibits_json as any)?.leads_require_payment === true +); + +// Stripe keys from mod_exhibits_json — passed to the payment component. +// The component falls back to site_cfg_json if these are null (legacy migration path). +let stripe_cfg = $derived({ + stripe_publishable_key: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_publishable_key ?? null, + stripe_btn_1_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_1_license ?? null, + stripe_btn_3_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_3_license ?? null, + stripe_btn_6_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_6_license ?? null, + stripe_btn_10_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_10_license ?? null, +}); + // Standardized Reactive Search Pattern let search_params = $derived.by(() => { let licensee_email = $events_loc.leads.tracking__qry__licensee_email; - // Resolve "My Leads" to actual 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. if (licensee_email === 'my') { - licensee_email = - $events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? ''] - ?.key || 'all'; + const kv = $events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? '']; + licensee_email = kv?.type === 'shared' + ? 'shared_passcode' + : kv?.key || 'all'; } return { @@ -440,8 +466,8 @@ function toggle_manage_tab() { {/if} - - {#if $ae_loc.show_leads_payment} + + {#if leads_require_payment} + {#if show_billing} + + {:else} + + {/if} + - {#if show_billing} -
- -
- {/if} - + {#if show_billing} +
+ +
+ {/if} + + {/if} @@ -462,14 +481,6 @@ function handle_signout() { class="checkbox" bind:checked={$ae_loc.auto_hide_nav} /> -