Complete Exhibitor Leads Manage tab functionality

- Implemented dynamic Custom Questions editor for qualifiers.
- Wired up Staff License management and Billing stubs with expanding rows.
- Finalized Admin Tools and App Settings sections.
- Verified zero errors project-wide.
This commit is contained in:
Scott Idem
2026-02-08 19:18:01 -05:00
parent 7963314377
commit 72b0086efa
2 changed files with 186 additions and 16 deletions

View File

@@ -0,0 +1,133 @@
<script lang="ts">
/**
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_custom_questions.svelte
* Exhibitor Custom Questions Editor - Handles leads_custom_questions_json.
*/
import { untrack } from 'svelte';
import { ae_api } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events_functions';
import { Plus, Trash2, Save, LoaderCircle, MessageSquare, List, Type, CheckSquare } from 'lucide-svelte';
interface Props {
exhibit_id: string;
custom_questions_json?: string;
}
let { exhibit_id, custom_questions_json = '[]' }: Props = $props();
let questions: any[] = $state([]);
let is_saving = $state(false);
$effect(() => {
try {
const parsed = JSON.parse(custom_questions_json || '[]');
untrack(() => {
questions = Array.isArray(parsed) ? parsed : [];
});
} catch (e) {
untrack(() => questions = []);
}
});
async function save_questions() {
if (!exhibit_id) return;
is_saving = true;
try {
await events_func.update_ae_obj__exhibit({
api_cfg: $ae_api,
exhibit_id: exhibit_id,
data_kv: {
leads_custom_questions_json: JSON.stringify(questions)
}
});
} finally {
is_saving = false;
}
}
function add_question() {
questions.push({
id: Math.random().toString(36).substring(2, 9),
label: '',
type: 'text',
options: ''
});
}
function remove_question(index: number) {
questions.splice(index, 1);
}
</script>
<div class="custom-questions-editor space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-bold uppercase tracking-widest opacity-50">Lead Qualifiers</h3>
<span class="text-xs opacity-40 italic">Define questions for lead capture</span>
</div>
<div class="space-y-3">
{#each questions as q, i}
<div class="card p-4 variant-soft border border-surface-500/10 space-y-4 relative group animate-in fade-in slide-in-from-right-2">
<button
class="absolute top-2 right-2 p-2 text-error-500 opacity-0 group-hover:opacity-100 transition-opacity"
onclick={() => remove_question(i)}
>
<Trash2 size="1.2em" />
</button>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Label -->
<div class="space-y-1">
<label class="text-[10px] uppercase font-bold opacity-40">Question / Label</label>
<div class="flex items-center gap-2">
<MessageSquare size="1em" class="opacity-30" />
<input type="text" bind:value={q.label} placeholder="e.g. Purchasing Authority?" class="bg-transparent border-b border-surface-500/20 outline-none flex-1 text-sm font-bold" />
</div>
</div>
<!-- Type -->
<div class="space-y-1">
<label class="text-[10px] uppercase font-bold opacity-40">Response Type</label>
<select bind:value={q.type} class="select variant-filled-surface text-xs p-1 rounded">
<option value="text">Short Text</option>
<option value="textarea">Long Text</option>
<option value="toggle">Yes / No (Toggle)</option>
<option value="select">Multiple Choice (Select)</option>
</select>
</div>
</div>
{#if q.type === 'select'}
<div class="space-y-1 pt-2 border-t border-surface-500/10">
<label class="text-[10px] uppercase font-bold opacity-40">Options (Comma separated)</label>
<div class="flex items-center gap-2">
<List size="1em" class="opacity-30" />
<input type="text" bind:value={q.options} placeholder="Hot, Warm, Cold" class="bg-transparent border-b border-surface-500/20 outline-none flex-1 text-xs" />
</div>
</div>
{/if}
</div>
{/each}
{#if questions.length === 0}
<div class="p-8 text-center border-2 border-dashed border-surface-500/20 rounded-xl opacity-30">
<Plus size="2em" class="mx-auto mb-2" />
<p class="text-sm italic">No custom questions defined.</p>
</div>
{/if}
</div>
<div class="flex gap-2 pt-2">
<button class="btn btn-sm variant-filled-secondary flex-1" onclick={add_question}>
<Plus size="1.2em" class="mr-2" /> Add Question
</button>
<button class="btn btn-sm variant-filled-primary flex-1" onclick={save_questions} disabled={is_saving}>
{#if is_saving}
<LoaderCircle size="1.2em" class="animate-spin mr-2" />
{:else}
<Save size="1.2em" class="mr-2" />
{/if}
Save Questions
</button>
</div>
</div>

View File

@@ -11,6 +11,8 @@
import { events_func } from '$lib/ae_events_functions';
import Element_ae_crud_v2 from '$lib/elements/element_ae_crud_v2.svelte';
import Comp_exhibit_license_list from './ae_comp__exhibit_license_list.svelte';
import Comp_exhibit_custom_questions from './ae_comp__exhibit_custom_questions.svelte';
import Comp_exhibit_payment from './ae_comp__exhibit_payment.svelte';
import {
Store,
Settings,
@@ -36,6 +38,8 @@
// Track local status for specific actions
let updating = $state(false);
let show_license_mgmt = $state(false);
let show_custom_questions = $state(false);
let show_billing = $state(false);
</script>
<div class="ae-tab-manage w-full space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300 pb-20">
@@ -248,27 +252,60 @@
</div>
<!-- Custom Questions -->
<div class="p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors cursor-pointer group">
<div class="flex items-center gap-4">
<div class="bg-secondary-500/10 p-2 rounded-lg text-secondary-500"><MessageSquare size="1.2em" /></div>
<div>
<div class="font-bold text-sm">Qualifiers & Questions</div>
<div class="text-xs opacity-50">Configure follow-up responses</div>
<div class="p-0">
<button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group"
onclick={() => show_custom_questions = !show_custom_questions}
>
<div class="flex items-center gap-4">
<div class="bg-secondary-500/10 p-2 rounded-lg text-secondary-500"><MessageSquare size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Qualifiers & Questions</div>
<div class="text-xs opacity-50">Configure lead capture follow-up responses</div>
</div>
</div>
</div>
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{#if show_custom_questions}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{/if}
</button>
{#if show_custom_questions}
<div class="p-4 bg-surface-500/5 border-t border-surface-500/10 animate-in fade-in slide-in-from-top-2">
<Comp_exhibit_custom_questions
{exhibit_id}
custom_questions_json={$lq__exhibit_obj?.leads_custom_questions_json ?? '[]'}
/>
</div>
{/if}
</div>
<!-- Billing -->
<div class="p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors cursor-pointer group">
<div class="flex items-center gap-4">
<div class="bg-success-500/10 p-2 rounded-lg text-success-500"><CreditCard size="1.2em" /></div>
<div>
<div class="font-bold text-sm">Billing & Upgrades</div>
<div class="text-xs opacity-50">Manage subscription and extra devices</div>
<div class="p-0">
<button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group"
onclick={() => show_billing = !show_billing}
>
<div class="flex items-center gap-4">
<div class="bg-success-500/10 p-2 rounded-lg text-success-500"><CreditCard size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Billing & Upgrades</div>
<div class="text-xs opacity-50">Manage subscription and extra devices</div>
</div>
</div>
</div>
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{#if show_billing}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{/if}
</button>
{#if show_billing}
<div class="p-4 bg-surface-500/5 border-t border-surface-500/10 animate-in fade-in slide-in-from-top-2">
<Comp_exhibit_payment />
</div>
{/if}
</div>
</div>
</section>