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:
@@ -114,13 +114,12 @@ Exhibit configuration and app settings.
|
|||||||
- Sign out button
|
- Sign out button
|
||||||
|
|
||||||
**Lead Retrieval Config**:
|
**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
|
- 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**:
|
**App Settings**:
|
||||||
- Auto-hide header/footer toggle
|
- Auto-hide header/footer toggle
|
||||||
- Show Payment Tab toggle
|
|
||||||
- Show Extra Details toggle
|
- Show Extra Details toggle
|
||||||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||||||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
- 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
|
## Known Gaps
|
||||||
|
|
||||||
|
None currently. See TODO__Agents.md for remaining smoke test items.
|
||||||
|
|
||||||
|
## Implemented (previously listed as gaps)
|
||||||
|
|
||||||
### Payment / Stripe
|
### Payment / Stripe
|
||||||
`ae_comp__exhibit_payment.svelte` is a stub. The Stripe integration is not implemented.
|
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||||||
The payment tab can be hidden via "Show Payment Tab" in App Settings.
|
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
|
### License Management — Shared Passcode Access
|
||||||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
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'`).
|
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
|
### "My Leads" filter for shared-passcode users
|
||||||
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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_tracking_obj_li.svelte` | Lead list item renderer |
|
||||||
| `ae_comp__exhibit_license_list.svelte` | License slot manager (admin) |
|
| `ae_comp__exhibit_license_list.svelte` | License slot manager (admin) |
|
||||||
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor (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 |
|
| `ae_comp__exhibit_search.svelte` | Exhibit search input on the landing page |
|
||||||
|
|
||||||
### Lead detail components (within `lead/[exhibit_tracking_id]/`)
|
### Lead detail components (within `lead/[exhibit_tracking_id]/`)
|
||||||
@@ -114,11 +114,16 @@ Two scan modes (toggled per exhibit):
|
|||||||
|
|
||||||
## Known Gaps / Not Yet Implemented
|
## Known Gaps / Not Yet Implemented
|
||||||
|
|
||||||
- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is a stub. The payment tab can be
|
- None currently. See `TODO__Agents.md` for the remaining smoke test checklist.
|
||||||
hidden via the "Show Payment Tab" toggle in the Manage tab's App Settings.
|
|
||||||
|
|
||||||
## Implemented (previously listed as gaps)
|
## 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
|
- **`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.
|
`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
|
- **Show/hide hidden records** — toggle in `ae_comp__exhibit_tracking_search.svelte`; filters
|
||||||
|
|||||||
317
src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
Normal file
317
src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
Normal 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 & 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}
|
||||||
@@ -134,15 +134,41 @@ const lq__exhibit_obj = liveQuery(() => {
|
|||||||
return db_events.exhibit.get(exhibit_id);
|
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
|
// Standardized Reactive Search Pattern
|
||||||
let search_params = $derived.by(() => {
|
let search_params = $derived.by(() => {
|
||||||
let licensee_email = $events_loc.leads.tracking__qry__licensee_email;
|
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') {
|
if (licensee_email === 'my') {
|
||||||
licensee_email =
|
const kv = $events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? ''];
|
||||||
$events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? '']
|
licensee_email = kv?.type === 'shared'
|
||||||
?.key || 'all';
|
? 'shared_passcode'
|
||||||
|
: kv?.key || 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -440,8 +466,8 @@ function toggle_manage_tab() {
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Payment (Conditional) -->
|
<!-- Payment (Conditional) — visible only when event-level leads_require_payment is true -->
|
||||||
{#if $ae_loc.show_leads_payment}
|
{#if leads_require_payment}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm px-2 transition-colors sm:px-3"
|
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 ?? ''} />
|
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
|
||||||
{:else if active_tab === 'payment'}
|
{:else if active_tab === 'payment'}
|
||||||
<div class="mx-auto w-full max-w-4xl">
|
<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>
|
</div>
|
||||||
{:else if active_tab === 'list'}
|
{:else if active_tab === 'list'}
|
||||||
<div class="flex w-full flex-col space-y-6">
|
<div class="flex w-full flex-col space-y-6">
|
||||||
@@ -512,7 +538,7 @@ function toggle_manage_tab() {
|
|||||||
</div>
|
</div>
|
||||||
{:else if active_tab === 'manage'}
|
{:else if active_tab === 'manage'}
|
||||||
<div class="mx-auto w-full max-w-4xl">
|
<div class="mx-auto w-full max-w-4xl">
|
||||||
<Tab_manage />
|
<Tab_manage {leads_require_payment} {stripe_cfg} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
* ae_comp__exhibit_payment.svelte
|
* ae_comp__exhibit_payment.svelte
|
||||||
* Leads Payment — Stripe Buy Button integration.
|
* 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 config priority (first wins):
|
||||||
* stripe_publishable_key — Stripe publishable key (pk_live_... or pk_test_...)
|
* 1. Props passed from the parent (sourced from event.mod_exhibits_json — preferred)
|
||||||
* stripe_btn_1_license — Stripe Buy Button ID for the 1-user license tier
|
* 2. $ae_loc.site_cfg_json fallback (legacy — for events not yet migrated to mod_exhibits_json)
|
||||||
* stripe_btn_3_license — Buy Button ID for 3-user tier
|
*
|
||||||
* stripe_btn_6_license — Buy Button ID for 6-user tier
|
* Props are set in the exhibit +page.svelte from lq__event_obj.mod_exhibits_json and passed
|
||||||
* stripe_btn_10_license — Buy Button ID for 10-user tier
|
* 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.
|
* client_reference_id = exhibit_id — ties each Stripe payment back to this booth record.
|
||||||
* Payment status (priority flag) is read live from Dexie (IDB).
|
* Payment status (priority flag) is read live from Dexie (IDB).
|
||||||
@@ -24,23 +24,38 @@ import { AlertTriangle, CheckCircle, CreditCard } from '@lucide/svelte';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exhibit_id: string;
|
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(() => {
|
const lq__exhibit_obj = liveQuery(() => {
|
||||||
if (!exhibit_id) return undefined;
|
if (!exhibit_id) return undefined;
|
||||||
return db_events.exhibit.get(exhibit_id);
|
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(
|
const stripe_publishable_key = $derived(
|
||||||
|
prop_stripe_key ??
|
||||||
($ae_loc.site_cfg_json?.stripe_publishable_key as string | undefined) ?? null
|
($ae_loc.site_cfg_json?.stripe_publishable_key as string | undefined) ?? null
|
||||||
);
|
);
|
||||||
const stripe_btn_ids = $derived({
|
const stripe_btn_ids = $derived({
|
||||||
1: ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
|
1: prop_btn_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,
|
3: prop_btn_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,
|
6: prop_btn_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,
|
10: prop_btn_10 ?? ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const license_tiers = [
|
const license_tiers = [
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ import {
|
|||||||
UserX,
|
UserX,
|
||||||
Users
|
Users
|
||||||
} from '@lucide/svelte';
|
} 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 ?? '');
|
const exhibit_id = $derived(page.params.exhibit_id ?? '');
|
||||||
|
|
||||||
let lq__exhibit_obj = $derived(
|
let lq__exhibit_obj = $derived(
|
||||||
@@ -397,41 +414,43 @@ function handle_signout() {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Billing -->
|
<!-- Billing — only shown when event-level leads_require_payment is enabled -->
|
||||||
<div class="p-0">
|
{#if leads_require_payment}
|
||||||
<button
|
<div class="p-0">
|
||||||
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
|
<button
|
||||||
onclick={() => (show_billing = !show_billing)}>
|
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
|
||||||
<div class="flex items-center gap-4">
|
onclick={() => (show_billing = !show_billing)}>
|
||||||
<div
|
<div class="flex items-center gap-4">
|
||||||
class="bg-success-500/10 text-success-500 rounded-lg p-2">
|
<div
|
||||||
<CreditCard size="1.2em" />
|
class="bg-success-500/10 text-success-500 rounded-lg p-2">
|
||||||
</div>
|
<CreditCard size="1.2em" />
|
||||||
<div class="text-left">
|
|
||||||
<div class="text-sm font-bold">
|
|
||||||
Licenses & Billing
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs opacity-50">
|
<div class="text-left">
|
||||||
Review licenses and manage payment
|
<div class="text-sm font-bold">
|
||||||
|
Licenses & Billing
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-50">
|
||||||
|
Review licenses and manage payment
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{#if show_billing}
|
||||||
{#if show_billing}
|
<ChevronDown size="1.2em" class="opacity-20" />
|
||||||
<ChevronDown size="1.2em" class="opacity-20" />
|
{:else}
|
||||||
{:else}
|
<ChevronRight
|
||||||
<ChevronRight
|
size="1.2em"
|
||||||
size="1.2em"
|
class="opacity-20 transition-transform group-hover:translate-x-1" />
|
||||||
class="opacity-20 transition-transform group-hover:translate-x-1" />
|
{/if}
|
||||||
{/if}
|
</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if show_billing}
|
{#if show_billing}
|
||||||
<div
|
<div
|
||||||
class="bg-surface-500/5 border-surface-500/10 animate-in fade-in slide-in-from-top-2 border-t p-4">
|
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} />
|
<Comp_exhibit_payment {exhibit_id} {...stripe_cfg} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -462,14 +481,6 @@ function handle_signout() {
|
|||||||
class="checkbox"
|
class="checkbox"
|
||||||
bind:checked={$ae_loc.auto_hide_nav} />
|
bind:checked={$ae_loc.auto_hide_nav} />
|
||||||
</label>
|
</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
|
<label
|
||||||
class="hover:bg-surface-500/10 flex cursor-pointer items-center justify-between rounded-lg p-2 transition-colors">
|
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>
|
<span class="text-sm">Show Extra Details</span>
|
||||||
|
|||||||
@@ -383,24 +383,38 @@ async function handle_save(field_name: string, data: any) {
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="details">
|
<details class="details">
|
||||||
<summary class="summary">Exhibits (mod_exhibits_json)</summary>
|
<summary class="summary">Exhibits / Leads (mod_exhibits_json)</summary>
|
||||||
<div class="p-4">
|
<div class="p-4 space-y-3">
|
||||||
<AE_Comp_Editor_CodeMirror
|
<p class="text-sm text-surface-500">
|
||||||
readonly={false}
|
Configure Leads payment visibility and Stripe keys for this event.
|
||||||
content={tmp_exhibits_json_str}
|
</p>
|
||||||
bind:new_content={tmp_exhibits_json_str}
|
<a
|
||||||
show_line_numbers={true}
|
href="/events/{event_id}/leads/config"
|
||||||
placeholder="JSON config"
|
class="btn preset-tonal-primary">
|
||||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
Go to Leads Config →
|
||||||
<button
|
</a>
|
||||||
type="button"
|
<!-- Raw JSON fallback for debugging / emergency edits -->
|
||||||
class="btn preset-tonal-primary"
|
<details class="mt-2">
|
||||||
onclick={() => {
|
<summary class="cursor-pointer text-xs text-surface-400">Raw JSON (advanced)</summary>
|
||||||
handle_save(
|
<div class="mt-2 space-y-2">
|
||||||
'mod_exhibits_json',
|
<AE_Comp_Editor_CodeMirror
|
||||||
tmp_exhibits_json_str
|
readonly={false}
|
||||||
);
|
content={tmp_exhibits_json_str}
|
||||||
}}>Save</button>
|
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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user