Overhaul Exhibitor Leads Manage tab and resolve all TypeScript errors

- Implemented full Staff License management (CRUD for license_li_json).
- Added Admin Tools section for managers (Payment status, Max licenses, Device counts).
- Implemented App Settings (Refresh interval, navigation preferences, cache management).
- Fixed all remaining TypeScript errors in Badge and Presentation modules.
- Integrated Payment tab conditional visibility logic.
This commit is contained in:
Scott Idem
2026-02-08 18:46:32 -05:00
parent d6480bd0dc
commit 7963314377
6 changed files with 382 additions and 25 deletions

View File

@@ -9,7 +9,9 @@ import * as event_file from '$lib/ae_events/ae_events__event_file';
import {
load_ae_obj_id__exhibit,
load_ae_obj_li__exhibit,
search__exhibit
search__exhibit,
create_ae_obj__exhibit,
update_ae_obj__exhibit
} from '$lib/ae_events/ae_events__exhibit';
import {
@@ -74,6 +76,8 @@ const export_obj = {
load_ae_obj_id__exhibit: load_ae_obj_id__exhibit,
load_ae_obj_li__exhibit: load_ae_obj_li__exhibit,
search__exhibit: search__exhibit,
create_ae_obj__exhibit: create_ae_obj__exhibit,
update_ae_obj__exhibit: update_ae_obj__exhibit,
load_ae_obj_id__exhibit_tracking: load_ae_obj_id__exhibit_tracking,
load_ae_obj_li__exhibit_tracking: load_ae_obj_li__exhibit_tracking,
search__exhibit_tracking: search__exhibit_tracking,

View File

@@ -183,10 +183,10 @@
</a>
</header>
{#if $lq__event_badge_obj}
{#if $lq__event_badge_obj && $lq__event_badge_obj.event_id && event_badge_id}
<Comp_badge_obj_view
event_id={$lq__event_badge_obj.event_id}
{event_badge_id}
event_id={$lq__event_badge_obj.event_id as string}
event_badge_id={event_badge_id as string}
{lq__event_badge_obj}
{is_review_mode}
{lq__event_badge_template_obj}

View File

@@ -26,6 +26,8 @@
import Tab_add from './ae_tab__add.svelte';
import Tab_start from './ae_tab__start.svelte';
import Tab_manage from './ae_tab__manage.svelte';
import Comp_exhibit_payment from './ae_comp__exhibit_payment.svelte';
import { CreditCard } from 'lucide-svelte';
// *** Initialization & Store Guard ***
if ($events_loc.leads) {
@@ -39,6 +41,8 @@
$events_loc.leads.tracking__qry__search_text = '';
if (typeof $events_loc.leads.tracking__qry__sort_order === 'undefined')
$events_loc.leads.tracking__qry__sort_order = 'created_desc';
if (typeof $events_loc.leads.refresh_interval_sec === 'undefined')
$events_loc.leads.refresh_interval_sec = 25;
}
// --- Tab State (Sticky via Store) ---
@@ -322,6 +326,20 @@
{/if}
</button>
<!-- Payment (Conditional) -->
{#if $ae_loc.show_leads_payment}
<button
type="button"
class="btn btn-sm transition-colors px-2 sm:px-3"
class:variant-filled-success={active_tab === 'payment'}
class:variant-ghost-success={active_tab !== 'payment'}
onclick={() => set_active_tab('payment')}
title="Payment & Upgrades"
>
<CreditCard size="1.25em" />
</button>
{/if}
<!-- Manage / Config -->
<button
type="button"
@@ -345,6 +363,10 @@
</div>
{:else if active_tab === 'add'}
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
{:else if active_tab === 'payment'}
<div class="w-full max-w-4xl mx-auto">
<Comp_exhibit_payment />
</div>
{:else if active_tab === 'list'}
<div class="w-full flex flex-col space-y-6">
<div class="flex justify-between items-center px-2">

View File

@@ -1,11 +1,160 @@
<script lang="ts">
/**
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_license_list.svelte
* Exhibitor License Management Stub.
* Exhibitor License Management - Handles parsing and editing event_exhibit.license_li_json
*/
import { untrack } from 'svelte';
import { ae_api } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events_functions';
import { Plus, Trash2, Mail, Key, User, Save, LoaderCircle, Users } from 'lucide-svelte';
interface Props {
exhibit_id: string;
license_li_json?: string; // Raw JSON string from DB
license_max?: number;
}
let { exhibit_id, license_li_json = '[]', license_max = 0 }: Props = $props();
// Local state for the parsed list
let local_license_li: any[] = $state([]);
let is_saving = $state(false);
// Parse JSON into local state
$effect(() => {
try {
const parsed = JSON.parse(license_li_json || '[]');
untrack(() => {
local_license_li = Array.isArray(parsed) ? parsed : [];
});
} catch (e) {
console.error('Failed to parse license_li_json', e);
untrack(() => {
local_license_li = [];
});
}
});
async function save_licenses() {
if (!exhibit_id) return;
is_saving = true;
try {
const json_str = JSON.stringify(local_license_li);
await events_func.update_ae_obj__exhibit({
api_cfg: $ae_api,
exhibit_id: exhibit_id,
data_kv: {
license_li_json: json_str
}
});
} catch (e) {
console.error('Failed to save licenses', e);
} finally {
is_saving = false;
}
}
function add_license() {
if (local_license_li.length >= (license_max || 1)) {
alert(`Maximum licenses (${license_max}) reached.`);
return;
}
local_license_li.push({
full_name: '',
email: '',
passcode: Math.random().toString(36).substring(2, 8).toUpperCase()
});
}
function remove_license(index: number) {
if (confirm('Remove this license? The user will lose access immediately.')) {
local_license_li.splice(index, 1);
}
}
</script>
<div class="exhibit-license-list p-4 card">
<h3 class="h3">Staff Licenses</h3>
<p>Placeholder for license assignment logic.</p>
</div>
<div class="exhibit-license-list space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-bold uppercase tracking-widest opacity-50">Assigned Licenses</h3>
<span class="text-xs font-mono bg-surface-500/10 px-2 py-1 rounded">
{local_license_li.length} / {license_max || 1}
</span>
</div>
<div class="space-y-3">
{#each local_license_li as license, i}
<div class="card p-4 variant-soft border border-surface-500/10 space-y-3 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_license(i)}
title="Remove License"
>
<Trash2 size="1.2em" />
</button>
<!-- Name -->
<div class="flex items-center gap-3">
<User size="1.2em" class="opacity-30" />
<input
type="text"
bind:value={license.full_name}
placeholder="Full Name"
class="bg-transparent border-b border-surface-500/20 focus:border-primary-500 outline-none flex-1 text-sm font-bold"
/>
</div>
<!-- Email -->
<div class="flex items-center gap-3">
<Mail size="1.2em" class="opacity-30" />
<input
type="email"
bind:value={license.email}
placeholder="email@example.com"
class="bg-transparent border-b border-surface-500/20 focus:border-primary-500 outline-none flex-1 text-sm"
/>
</div>
<!-- Passcode -->
<div class="flex items-center gap-3">
<Key size="1.2em" class="opacity-30" />
<input
type="text"
bind:value={license.passcode}
placeholder="PASSCODE"
class="bg-transparent border-b border-surface-500/20 focus:border-primary-500 outline-none w-32 text-sm font-mono font-bold"
/>
</div>
</div>
{/each}
{#if local_license_li.length === 0}
<div class="p-8 text-center border-2 border-dashed border-surface-500/20 rounded-xl opacity-30">
<Users size="2em" class="mx-auto mb-2" />
<p class="text-sm italic">No licenses assigned yet.</p>
</div>
{/if}
</div>
<div class="flex gap-2 pt-2">
<button
class="btn btn-sm variant-filled-secondary flex-1"
onclick={add_license}
disabled={local_license_li.length >= (license_max || 1)}
>
<Plus size="1.2em" class="mr-2" /> Add Staff License
</button>
<button
class="btn btn-sm variant-filled-primary flex-1"
onclick={save_licenses}
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 Changes
</button>
</div>
</div>

View File

@@ -7,8 +7,10 @@
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
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 {
Store,
Settings,
@@ -18,7 +20,8 @@
CreditCard,
Key,
Users,
ChevronRight
ChevronRight,
ChevronDown
} from 'lucide-svelte';
const exhibit_id = $derived(page.params.exhibit_id ?? '');
@@ -32,10 +35,87 @@
// Track local status for specific actions
let updating = $state(false);
let show_license_mgmt = $state(false);
</script>
<div class="ae-tab-manage w-full space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div class="ae-tab-manage w-full space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300 pb-20">
<!-- Section: Admin Tools (Manager Access Only) -->
{#if $ae_loc.manager_access}
<section class="space-y-4 p-4 border-2 border-primary-500/20 rounded-xl bg-primary-500/5">
<div class="flex items-center gap-2 border-b border-primary-500/10 pb-2">
<Settings size="1.2em" class="text-primary-500" />
<h3 class="text-lg font-bold uppercase tracking-wider text-primary-500">Admin Tools</h3>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Priority / Payment Toggle -->
<div class="card p-3 variant-soft flex items-center justify-between">
<div>
<div class="text-[10px] uppercase font-black opacity-40">Payment Status</div>
<div class="font-bold">{$lq__exhibit_obj?.priority ? 'PAID' : 'PENDING'}</div>
</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
object_id={exhibit_id}
field_name="priority"
field_type="boolean"
current_field_value={$lq__exhibit_obj?.priority}
/>
</div>
<!-- Max Licenses -->
<div class="card p-3 variant-soft flex items-center justify-between">
<div>
<div class="text-[10px] uppercase font-black opacity-40">Max Licenses</div>
</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
object_id={exhibit_id}
field_name="license_max"
field_type="number"
current_field_value={$lq__exhibit_obj?.license_max}
class_li="w-16 font-mono text-right"
/>
</div>
<!-- Small Devices -->
<div class="card p-3 variant-soft flex items-center justify-between">
<div>
<div class="text-[10px] uppercase font-black opacity-40">Small Devices</div>
</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
object_id={exhibit_id}
field_name="leads_device_sm_qty"
field_type="number"
current_field_value={$lq__exhibit_obj?.leads_device_sm_qty}
class_li="w-16 font-mono text-right"
/>
</div>
<!-- Large Devices -->
<div class="card p-3 variant-soft flex items-center justify-between">
<div>
<div class="text-[10px] uppercase font-black opacity-40">Large Devices</div>
</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
object_id={exhibit_id}
field_name="leads_device_lg_qty"
field_type="number"
current_field_value={$lq__exhibit_obj?.leads_device_lg_qty}
class_li="w-16 font-mono text-right"
/>
</div>
</div>
</section>
{/if}
<!-- Section: Booth Profile -->
<section class="space-y-4">
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-2">
@@ -95,9 +175,19 @@
<!-- Staff Passcode -->
<div class="card p-4 bg-surface-500/5 border border-surface-500/10">
<div class="flex items-center justify-between">
<div>
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Staff Passcode</div>
<div class="font-mono text-xl tracking-widest font-bold">{$lq__exhibit_obj?.staff_passcode || '----'}</div>
<div class="flex-1">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest mb-1">Staff Passcode</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
object_id={exhibit_id}
field_name="staff_passcode"
field_type="text"
current_field_value={$lq__exhibit_obj?.staff_passcode}
class_li="font-mono text-xl tracking-widest font-bold"
hide_element={false}
display_block={true}
/>
</div>
<Key size="1.5em" class="opacity-20" />
</div>
@@ -127,15 +217,34 @@
<div class="card p-0 divide-y divide-surface-500/10 overflow-hidden shadow-md">
<!-- Licenses -->
<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-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div>
<div class="font-bold text-sm">Staff Licenses</div>
<div class="text-xs opacity-50">Active: 0 / Max: {$lq__exhibit_obj?.license_max || 1}</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_license_mgmt = !show_license_mgmt}
>
<div class="flex items-center gap-4">
<div class="bg-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Staff Licenses</div>
<div class="text-xs opacity-50">Manage assigned staff and codes</div>
</div>
</div>
</div>
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{#if show_license_mgmt}
<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_license_mgmt}
<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_license_list
{exhibit_id}
license_li_json={$lq__exhibit_obj?.license_li_json ?? '[]'}
license_max={$lq__exhibit_obj?.license_max}
/>
</div>
{/if}
</div>
<!-- Custom Questions -->
@@ -164,6 +273,77 @@
</div>
</section>
<!-- Section: App Settings -->
<section class="space-y-4">
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-2">
<Settings size="1.2em" class="text-secondary-500" />
<h3 class="text-lg font-bold uppercase tracking-wider">App Settings</h3>
</div>
<div class="card p-4 space-y-6 variant-soft shadow-inner">
<!-- Interface Prefs -->
<div class="space-y-3">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Interface Preferences</div>
<div class="grid grid-cols-1 gap-2">
<label class="flex items-center justify-between p-2 hover:bg-surface-500/10 rounded-lg cursor-pointer transition-colors">
<span class="text-sm">Auto-hide Header/Footer</span>
<input type="checkbox" class="checkbox" bind:checked={$ae_loc.auto_hide_nav} />
</label>
<label class="flex items-center justify-between p-2 hover:bg-surface-500/10 rounded-lg cursor-pointer 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="flex items-center justify-between p-2 hover:bg-surface-500/10 rounded-lg cursor-pointer transition-colors">
<span class="text-sm">Show Extra Details</span>
<input type="checkbox" class="checkbox" bind:checked={$events_loc.show_details} />
</label>
</div>
</div>
<!-- List Refresh -->
<div class="space-y-3">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Data Synchronization</div>
<div class="flex items-center gap-4 p-2 bg-surface-500/5 rounded-lg border border-surface-500/10">
<span class="text-sm flex-1">Refresh Interval (sec)</span>
<input
type="number"
class="input w-20 text-right font-mono p-1 bg-transparent border-b border-surface-500/20"
min="1"
max="120"
bind:value={$events_loc.leads.refresh_interval_sec}
placeholder="25"
/>
</div>
</div>
<!-- Maintenance -->
<div class="space-y-3">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Maintenance & Reset</div>
<div class="grid grid-cols-2 gap-2">
<button class="btn btn-sm variant-filled-warning" onclick={() => window.location.reload()}>
<span class="fas fa-sync mr-2"></span> Reload App
</button>
<button class="btn btn-sm variant-ghost-error" onclick={() => {
if(confirm('Clear all local cached data (IDB)?')) {
db_events.delete().then(() => window.location.reload());
}
}}>
<span class="fas fa-database mr-2"></span> Clear IDB
</button>
<button class="btn btn-sm variant-ghost-error col-span-2" onclick={() => {
if(confirm('Reset all local app settings and sign out?')) {
localStorage.clear();
window.location.reload();
}
}}>
<span class="fas fa-user-slash mr-2"></span> Clear Local Settings (Hard Reset)
</button>
</div>
</div>
</div>
</section>
<!-- Help Footer -->
<div class="pt-10 pb-20 text-center space-y-2 opacity-40">
<p class="text-xs">Exhibitor Management Module v3.0</p>

View File

@@ -155,11 +155,13 @@
// JSON formatted configuration options for an event, and specifically for the presentation management module.
$effect(() => {
if ($lq__event_obj?.mod_pres_mgmt_json) {
const remote_cfg = $lq__event_obj?.mod_pres_mgmt_json;
const local_cfg = $events_loc?.pres_mgmt;
if (remote_cfg && local_cfg) {
untrack(() => {
events_func.sync_config__event_pres_mgmt({
pres_mgmt_cfg_remote: $lq__event_obj?.mod_pres_mgmt_json,
pres_mgmt_cfg_local: $events_loc?.pres_mgmt,
pres_mgmt_cfg_remote: remote_cfg,
pres_mgmt_cfg_local: local_cfg,
log_lvl: log_lvl
});
});