leads: event-level payment config + Stripe key migration

- New /events/[event_id]/leads/config page: administrator UI for
  mod_exhibits_json. Controls leads_require_payment toggle and Stripe
  keys (publishable key + buy button IDs per license tier).

- leads_require_payment (mod_exhibits_json) now gates all billing UI:
  header CreditCard button in exhibit +page.svelte and Licenses & Billing
  accordion in ae_tab__manage.svelte. Default false (client covers costs).

- Stripe keys migrated from site_cfg_json to mod_exhibits_json (per-event).
  ae_comp__exhibit_payment accepts them as optional props; falls back to
  site_cfg_json for events not yet migrated.

- Fixed "My Leads" bug for shared-passcode users: search_params now maps
  licensee_email 'my' → 'shared_passcode' literal (not kv.key passcode
  string) so filters correctly match stored external_person_id values.

- Event settings: Exhibits section replaced with config link + raw JSON
  fallback, matching pres_mgmt/badges pattern.

- Docs updated: README.md, MODULE__AE_Events_Exhibitor_Leads.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-03 12:36:36 -04:00
parent 48c5515131
commit 7f79c1857a
7 changed files with 486 additions and 89 deletions

View File

@@ -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 (1120 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.
---

View File

@@ -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

View File

@@ -0,0 +1,317 @@
<script lang="ts">
/**
* Leads / Exhibits Config Page
* Route: /events/[event_id]/leads/config
*
* Admin UI for managing event.mod_exhibits_json (ExhibitsRemoteCfg).
* Access: administrator_access only.
*
* Controls:
* - leads_require_payment — show/hide Stripe billing UI event-wide
* - Stripe keys — publishable key + buy button IDs per license tier
* (previously lived in site_cfg_json; moved here so they
* are per-event rather than per-site)
*
* Save pattern: load → merge draft → PATCH event.mod_exhibits_json via V3 API.
*/
import { untrack } from 'svelte';
import { page } from '$app/state';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { api } from '$lib/api/api';
import {
AlertTriangle,
ArrowLeft,
Check,
ChevronDown,
ChevronUp,
CreditCard,
Info,
Lock,
Save,
Settings
} from '@lucide/svelte';
interface ExhibitsRemoteCfg {
/** When true, Stripe billing UI (header CreditCard button + Manage tab accordion) is visible. */
leads_require_payment: boolean;
/** Stripe publishable key (pk_live_... or pk_test_...). */
stripe_publishable_key: string | null;
/** Stripe Buy Button IDs per license tier. */
stripe_btn_1_license: string | null;
stripe_btn_3_license: string | null;
stripe_btn_6_license: string | null;
stripe_btn_10_license: string | null;
}
let event_id = $derived(page.params.event_id ?? '');
let lq__event_obj = $derived(
liveQuery(async () => {
if (!event_id) return null;
return await db_events.event.get(event_id);
})
);
// ---------------------------------------------------------------------------
// Draft state — initialized from the live event config
// ---------------------------------------------------------------------------
const cfg_defaults: ExhibitsRemoteCfg = {
leads_require_payment: false,
stripe_publishable_key: null,
stripe_btn_1_license: null,
stripe_btn_3_license: null,
stripe_btn_6_license: null,
stripe_btn_10_license: null
};
let draft: ExhibitsRemoteCfg = $state({ ...cfg_defaults });
let draft_initialized = $state(false);
let initial_json = $state('');
$effect(() => {
const event_obj = $lq__event_obj;
if (event_obj && !draft_initialized) {
untrack(() => {
const saved = (event_obj.mod_exhibits_json ?? {}) as Partial<ExhibitsRemoteCfg>;
draft = { ...cfg_defaults, ...saved };
initial_json = JSON.stringify(draft);
draft_initialized = true;
});
}
});
let is_dirty = $derived(draft_initialized && JSON.stringify(draft) !== initial_json);
// Section collapse state
let sections: Record<string, boolean> = $state({
payment: true,
stripe: true
});
function toggle(key: string) {
sections[key] = !sections[key];
}
// ---------------------------------------------------------------------------
// Save
// ---------------------------------------------------------------------------
let save_status: 'idle' | 'saving' | 'success' | 'error' = $state('idle');
async function save() {
if (!event_id) return;
save_status = 'saving';
try {
// Preserve any unknown keys already in mod_exhibits_json (future-proofing).
const current_cfg = ($lq__event_obj?.mod_exhibits_json ?? {}) as any;
const new_cfg = { ...current_cfg, ...draft };
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event',
obj_id: event_id,
fields: { mod_exhibits_json: new_cfg },
log_lvl: 1
});
// Reload event so consumers pick up the new config immediately.
await events_func.load_ae_obj_id__event({
api_cfg: $ae_api,
event_id: event_id,
log_lvl: 1
});
initial_json = JSON.stringify(draft);
save_status = 'success';
setTimeout(() => (save_status = 'idle'), 3000);
} catch (e) {
console.error('Failed to save exhibits config', e);
save_status = 'error';
setTimeout(() => (save_status = 'idle'), 5000);
}
}
</script>
<svelte:head>
<title>Leads Config</title>
</svelte:head>
{#if !$ae_loc.administrator_access}
<div class="p-8 text-center opacity-50">
<Lock size="3em" class="mx-auto mb-2" />
<p>Administrator access required.</p>
</div>
{:else}
<div class="mx-auto w-full max-w-2xl space-y-4 px-2 py-4">
<!-- Header -->
<header class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
<a
href="/events/{event_id}/leads"
class="btn btn-sm preset-tonal-surface"
title="Back to Leads">
<ArrowLeft size="1em" />
</a>
<Settings size="1.2em" class="text-primary-500" />
<h1 class="text-xl font-bold">Leads Config</h1>
</div>
<div class="flex items-center gap-2">
{#if save_status === 'success'}
<span class="badge preset-tonal-success flex items-center gap-1">
<Check size="1em" /> Saved
</span>
{:else if save_status === 'error'}
<span class="badge preset-tonal-error flex items-center gap-1">
<AlertTriangle size="1em" /> Error saving
</span>
{/if}
<button
type="button"
class="btn preset-filled-primary-500"
onclick={save}
disabled={!is_dirty || save_status === 'saving'}>
<Save size="1em" class="mr-1" />
{save_status === 'saving' ? 'Saving...' : 'Save'}
</button>
</div>
</header>
<p class="text-surface-500 text-sm">
Changes here update <code>event.mod_exhibits_json</code> and take effect immediately
for all exhibitors at this event (no re-login required).
</p>
{#if !draft_initialized}
<p class="text-surface-400 italic">Loading event config...</p>
{:else}
<!-- ================================================================ -->
<!-- PAYMENT -->
<!-- ================================================================ -->
<section class="border-surface-200-800 rounded-xl border">
<button
type="button"
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
onclick={() => toggle('payment')}>
<span class="flex items-center gap-2">
<CreditCard size="1em" class="text-success-500" />
Payment
</span>
{#if sections.payment}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
</button>
{#if sections.payment}
<div class="border-surface-200-800 space-y-3 border-t px-4 py-4">
<label class="flex items-start gap-3">
<input
type="checkbox"
class="checkbox mt-0.5"
bind:checked={draft.leads_require_payment} />
<span>
<span class="font-semibold">Require Payment (Stripe)</span>
<span class="text-surface-500 ml-2 text-sm">
Shows the Stripe billing UI to exhibitors — the header CreditCard
button and the "Licenses &amp; Billing" accordion in the Manage tab.
Leave <strong>off</strong> when the client is covering costs.
</span>
</span>
</label>
<div class="flex items-start gap-2 rounded-lg bg-surface-500/5 p-3 text-xs opacity-60">
<Info size="1em" class="mt-0.5 shrink-0" />
<span>
The Stripe payment component code is always present. This toggle only
controls visibility — no code changes needed when switching events.
</span>
</div>
</div>
{/if}
</section>
<!-- ================================================================ -->
<!-- STRIPE CONFIG -->
<!-- ================================================================ -->
<section class="border-surface-200-800 rounded-xl border">
<button
type="button"
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
onclick={() => toggle('stripe')}>
<span>
Stripe Keys
<span class="text-surface-500 text-xs font-normal ml-1">(per-event)</span>
</span>
{#if sections.stripe}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
</button>
{#if sections.stripe}
<div class="border-surface-200-800 space-y-4 border-t px-4 py-4">
<div class="flex items-start gap-2 text-xs opacity-60">
<Info size="1em" class="mt-0.5 shrink-0" />
<span>
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).
</span>
</div>
<label class="flex flex-col gap-1">
<span class="text-sm font-medium">Publishable Key</span>
<input
type="text"
class="input input-sm font-mono text-xs"
placeholder="pk_live_... or pk_test_..."
bind:value={draft.stripe_publishable_key} />
</label>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label class="flex flex-col gap-1">
<span class="text-sm font-medium">Buy Button — 1 License</span>
<input
type="text"
class="input input-sm font-mono text-xs"
placeholder="buy_btn_..."
bind:value={draft.stripe_btn_1_license} />
</label>
<label class="flex flex-col gap-1">
<span class="text-sm font-medium">Buy Button — 3 Licenses</span>
<input
type="text"
class="input input-sm font-mono text-xs"
placeholder="buy_btn_..."
bind:value={draft.stripe_btn_3_license} />
</label>
<label class="flex flex-col gap-1">
<span class="text-sm font-medium">Buy Button — 6 Licenses</span>
<input
type="text"
class="input input-sm font-mono text-xs"
placeholder="buy_btn_..."
bind:value={draft.stripe_btn_6_license} />
</label>
<label class="flex flex-col gap-1">
<span class="text-sm font-medium">Buy Button — 10 Licenses</span>
<input
type="text"
class="input input-sm font-mono text-xs"
placeholder="buy_btn_..."
bind:value={draft.stripe_btn_10_license} />
</label>
</div>
</div>
{/if}
</section>
<!-- Bottom save -->
<div class="flex justify-end">
<button
type="button"
class="btn preset-filled-primary-500"
onclick={save}
disabled={!is_dirty || save_status === 'saving'}>
<Save size="1em" class="mr-1" />
{save_status === 'saving' ? 'Saving...' : 'Save Config'}
</button>
</div>
{/if}
</div>
{/if}

View File

@@ -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}
</button>
<!-- Payment (Conditional) -->
{#if $ae_loc.show_leads_payment}
<!-- Payment (Conditional) — visible only when event-level leads_require_payment is true -->
{#if leads_require_payment}
<button
type="button"
class="btn btn-sm px-2 transition-colors sm:px-3"
@@ -478,7 +504,7 @@ function toggle_manage_tab() {
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
{:else if active_tab === 'payment'}
<div class="mx-auto w-full max-w-4xl">
<Comp_exhibit_payment exhibit_id={page.params.exhibit_id ?? ''} />
<Comp_exhibit_payment exhibit_id={page.params.exhibit_id ?? ''} {...stripe_cfg} />
</div>
{:else if active_tab === 'list'}
<div class="flex w-full flex-col space-y-6">
@@ -512,7 +538,7 @@ function toggle_manage_tab() {
</div>
{:else if active_tab === 'manage'}
<div class="mx-auto w-full max-w-4xl">
<Tab_manage />
<Tab_manage {leads_require_payment} {stripe_cfg} />
</div>
{/if}
</div>

View File

@@ -3,12 +3,12 @@
* ae_comp__exhibit_payment.svelte
* Leads Payment — Stripe Buy Button integration.
*
* Stripe config is read from $ae_loc.site_cfg_json (set per-event by admin in Site Settings):
* stripe_publishable_key — Stripe publishable key (pk_live_... or pk_test_...)
* stripe_btn_1_license — Stripe Buy Button ID for the 1-user license tier
* stripe_btn_3_license — Buy Button ID for 3-user tier
* stripe_btn_6_license — Buy Button ID for 6-user tier
* stripe_btn_10_license — Buy Button ID for 10-user tier
* Stripe config priority (first wins):
* 1. Props passed from the parent (sourced from event.mod_exhibits_json — preferred)
* 2. $ae_loc.site_cfg_json fallback (legacy — for events not yet migrated to mod_exhibits_json)
*
* Props are set in the exhibit +page.svelte from lq__event_obj.mod_exhibits_json and passed
* down so this component doesn't need to know the event_id or do its own Dexie query.
*
* client_reference_id = exhibit_id — ties each Stripe payment back to this booth record.
* Payment status (priority flag) is read live from Dexie (IDB).
@@ -24,23 +24,38 @@ import { AlertTriangle, CheckCircle, CreditCard } from '@lucide/svelte';
interface Props {
exhibit_id: string;
/** From event.mod_exhibits_json — set in the Leads Config page. Falls back to site_cfg_json. */
stripe_publishable_key?: string | null;
stripe_btn_1_license?: string | null;
stripe_btn_3_license?: string | null;
stripe_btn_6_license?: string | null;
stripe_btn_10_license?: string | null;
}
let { exhibit_id }: Props = $props();
let {
exhibit_id,
stripe_publishable_key: prop_stripe_key = null,
stripe_btn_1_license: prop_btn_1 = null,
stripe_btn_3_license: prop_btn_3 = null,
stripe_btn_6_license: prop_btn_6 = null,
stripe_btn_10_license: prop_btn_10 = null
}: Props = $props();
const lq__exhibit_obj = liveQuery(() => {
if (!exhibit_id) return undefined;
return db_events.exhibit.get(exhibit_id);
});
// Stripe config from site_cfg_json — set per-event by admin in Site Settings.
// Stripe config: prefer event-level props (mod_exhibits_json); fall back to site_cfg_json
// for events that haven't been migrated to the per-event config page yet.
const stripe_publishable_key = $derived(
prop_stripe_key ??
($ae_loc.site_cfg_json?.stripe_publishable_key as string | undefined) ?? null
);
const stripe_btn_ids = $derived({
1: ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
3: ($ae_loc.site_cfg_json?.stripe_btn_3_license as string | undefined) ?? null,
6: ($ae_loc.site_cfg_json?.stripe_btn_6_license as string | undefined) ?? null,
10: ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
1: prop_btn_1 ?? ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
3: prop_btn_3 ?? ($ae_loc.site_cfg_json?.stripe_btn_3_license as string | undefined) ?? null,
6: prop_btn_6 ?? ($ae_loc.site_cfg_json?.stripe_btn_6_license as string | undefined) ?? null,
10: prop_btn_10 ?? ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
});
const license_tiers = [

View File

@@ -30,6 +30,23 @@ import {
UserX,
Users
} from '@lucide/svelte';
interface StripeCfg {
stripe_publishable_key?: string | null;
stripe_btn_1_license?: string | null;
stripe_btn_3_license?: string | null;
stripe_btn_6_license?: string | null;
stripe_btn_10_license?: string | null;
}
// leads_require_payment: event-level flag from mod_exhibits_json.
// When false (default), all billing UI is hidden — client is covering costs.
// Set mod_exhibits_json.leads_require_payment = true in event settings to enable Stripe.
// stripe_cfg: Stripe keys from mod_exhibits_json, passed through to the payment component.
let {
leads_require_payment = false,
stripe_cfg = {}
}: { leads_require_payment?: boolean; stripe_cfg?: StripeCfg } = $props();
const exhibit_id = $derived(page.params.exhibit_id ?? '');
let lq__exhibit_obj = $derived(
@@ -397,41 +414,43 @@ function handle_signout() {
{/if}
</div>
<!-- Billing -->
<div class="p-0">
<button
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
onclick={() => (show_billing = !show_billing)}>
<div class="flex items-center gap-4">
<div
class="bg-success-500/10 text-success-500 rounded-lg p-2">
<CreditCard size="1.2em" />
</div>
<div class="text-left">
<div class="text-sm font-bold">
Licenses & Billing
<!-- Billing — only shown when event-level leads_require_payment is enabled -->
{#if leads_require_payment}
<div class="p-0">
<button
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
onclick={() => (show_billing = !show_billing)}>
<div class="flex items-center gap-4">
<div
class="bg-success-500/10 text-success-500 rounded-lg p-2">
<CreditCard size="1.2em" />
</div>
<div class="text-xs opacity-50">
Review licenses and manage payment
<div class="text-left">
<div class="text-sm font-bold">
Licenses & Billing
</div>
<div class="text-xs opacity-50">
Review licenses and manage payment
</div>
</div>
</div>
</div>
{#if show_billing}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight
size="1.2em"
class="opacity-20 transition-transform group-hover:translate-x-1" />
{/if}
</button>
{#if show_billing}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight
size="1.2em"
class="opacity-20 transition-transform group-hover:translate-x-1" />
{/if}
</button>
{#if show_billing}
<div
class="bg-surface-500/5 border-surface-500/10 animate-in fade-in slide-in-from-top-2 border-t p-4">
<Comp_exhibit_payment {exhibit_id} />
</div>
{/if}
</div>
{#if show_billing}
<div
class="bg-surface-500/5 border-surface-500/10 animate-in fade-in slide-in-from-top-2 border-t p-4">
<Comp_exhibit_payment {exhibit_id} {...stripe_cfg} />
</div>
{/if}
</div>
{/if}
</div>
</section>
@@ -462,14 +481,6 @@ function handle_signout() {
class="checkbox"
bind:checked={$ae_loc.auto_hide_nav} />
</label>
<label
class="hover:bg-surface-500/10 flex cursor-pointer items-center justify-between rounded-lg p-2 transition-colors">
<span class="text-sm">Show Payment Tab</span>
<input
type="checkbox"
class="checkbox"
bind:checked={$ae_loc.show_leads_payment} />
</label>
<label
class="hover:bg-surface-500/10 flex cursor-pointer items-center justify-between rounded-lg p-2 transition-colors">
<span class="text-sm">Show Extra Details</span>

View File

@@ -383,24 +383,38 @@ async function handle_save(field_name: string, data: any) {
</details>
<details class="details">
<summary class="summary">Exhibits (mod_exhibits_json)</summary>
<div class="p-4">
<AE_Comp_Editor_CodeMirror
readonly={false}
content={tmp_exhibits_json_str}
bind:new_content={tmp_exhibits_json_str}
show_line_numbers={true}
placeholder="JSON config"
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
<button
type="button"
class="btn preset-tonal-primary"
onclick={() => {
handle_save(
'mod_exhibits_json',
tmp_exhibits_json_str
);
}}>Save</button>
<summary class="summary">Exhibits / Leads (mod_exhibits_json)</summary>
<div class="p-4 space-y-3">
<p class="text-sm text-surface-500">
Configure Leads payment visibility and Stripe keys for this event.
</p>
<a
href="/events/{event_id}/leads/config"
class="btn preset-tonal-primary">
Go to Leads Config →
</a>
<!-- Raw JSON fallback for debugging / emergency edits -->
<details class="mt-2">
<summary class="cursor-pointer text-xs text-surface-400">Raw JSON (advanced)</summary>
<div class="mt-2 space-y-2">
<AE_Comp_Editor_CodeMirror
readonly={false}
content={tmp_exhibits_json_str}
bind:new_content={tmp_exhibits_json_str}
show_line_numbers={true}
placeholder="JSON config"
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
<button
type="button"
class="btn preset-tonal-primary"
onclick={() => {
handle_save(
'mod_exhibits_json',
tmp_exhibits_json_str
);
}}>Save Raw JSON</button>
</div>
</details>
</div>
</details>