feat(leads): stabilize v3 layout and unify tab width logic
- Removed max-width constraints across leads modules for full-width stability. - Fixed duplicate Download icon import causing build errors. - Improved header responsiveness for mobile-first experience. - Refined tab switching logic with state persistence placeholders.
This commit is contained in:
@@ -102,7 +102,7 @@ License:
|
||||
|
||||
## [tab 4] Manage / Config
|
||||
### Exhibit Specific
|
||||
* Priorty/payment toggle - Administrator Access or above
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
|
||||
@@ -78,27 +78,76 @@ I am probably using the term "tab" loosely here. It may just be sections that sh
|
||||
* Button to trigger QR scan (opens camera and scans QR code on badge)
|
||||
* Button to "Add as Lead" if Attendee Badge found and not already a Lead
|
||||
* Button to "View Lead" if Attendee Badge found and already a Lead
|
||||
Functions needed:
|
||||
* Search function to find Attendee Badge by Badge ID, QR code, name, email, or affiliations.
|
||||
* QR code scan function to read QR code and find Attendee Badge.
|
||||
* Add Lead function to create Exhibit_tracking entry linking Exhibit and Attendee Badge.
|
||||
|
||||
### [tab 3] Leads - List of Attendee Leads for Exhibitor
|
||||
* Allow for toggle between showing all per Exhibit and per licensed user based on their email address. Not perfect, but works well enough.
|
||||
* Allow for easy edit or remove
|
||||
* Sections:
|
||||
* List of Leads with basic info and buttons to Edit or Remove
|
||||
* Options:
|
||||
* Filter by Licensed user email address (dropdown of emails that have added leads for this Exhibit)
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Buttons and Inputs:
|
||||
* Button to Export Data - CSV or XLSX
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Should it have a text search?
|
||||
* NOTE: It is probably easiest for them to us the search tab to find a lead that has already been added. It will show "View Lead" button if already added.
|
||||
Functions needed:
|
||||
* Load Leads function to get Exhibit_tracking entries for the Exhibit.
|
||||
* Filter function to filter by Licensed user email address.
|
||||
* Sort function to sort by selected option.
|
||||
* Export function to export displayed Leads to CSV or XLSX.
|
||||
|
||||
### [tab 4] Manage - Leads (app and exhibit) Manage
|
||||
* Show list of Leads added for this Exhibit.
|
||||
* Allow for easy edit or remove
|
||||
* Allow for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Allow for toggle for show/hide Hidden records
|
||||
* Allow for filtering by Licensed user email address
|
||||
### [tab 4] Manage - Leads (app and exhibit) Manage / Config
|
||||
#### Exhibit Specific
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
* Exhibit (shared) Passcode
|
||||
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
|
||||
|
||||
#### App Specific
|
||||
|
||||
* Show/Hide Payment Tab
|
||||
* Additional Settings:
|
||||
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
|
||||
* Basic reload/refresh
|
||||
* Clear Indexed DB
|
||||
* Clear localStorage
|
||||
* Auto hide header/footer on sign in - default true
|
||||
* (?) Turn on iframe mode
|
||||
* (?) Show or hide additional details - Use "$events_loc.show_details"?
|
||||
|
||||
* Sections:
|
||||
* Exhibit Specific Manage/Config
|
||||
* App Specific Manage/Config
|
||||
* Buttons and Inputs:
|
||||
* Button to Export Data - CSV or XLSX
|
||||
* Toggle for show/hide Hidden records
|
||||
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
|
||||
* Filter by Licensed user email address (dropdown of emails that have added leads for this Exhibit)
|
||||
|
||||
|
||||
|
||||
* Exhibit Specific:
|
||||
* Priority/payment toggle - Administrator Access or above
|
||||
* Max licenses (number) - readonly or edit for Administrator Access or above
|
||||
* Small devices (number) - readonly or edit for Administrator Access or above
|
||||
* Large devices (number) - readonly or edit for Administrator Access or above
|
||||
* Exhibit (shared) Passcode
|
||||
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
|
||||
* App Specific:
|
||||
* Show/Hide Payment Tab
|
||||
* Show last refresh time and counter for next refresh based on the List refresh interval setting.
|
||||
* Additional Settings:
|
||||
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
|
||||
* Basic reload/refresh (F5)
|
||||
* Clear Indexed DB
|
||||
* Clear localStorage
|
||||
* Auto hide header/footer on sign in - default true
|
||||
* (?) Turn on iframe mode
|
||||
* (?) Show or hide additional details - Use "$events_loc.show_details"?
|
||||
* Functions:
|
||||
* Update Exhibit configuration function to update the Exhibit with the new settings.
|
||||
* Update App configuration function to update the app-wide settings for the Leads module.
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
// Basic layout for the leads module
|
||||
</script>
|
||||
|
||||
<div class="leads-module">
|
||||
<!-- <div class="leads-module"> -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
@@ -14,8 +14,7 @@ export async function load({ params, parent }) {
|
||||
if (browser && event_id) {
|
||||
events_func.load_ae_obj_li__exhibit({
|
||||
api_cfg: ae_acct.api,
|
||||
for_obj_type: 'event',
|
||||
for_obj_id: event_id,
|
||||
event_id: event_id,
|
||||
limit: 100,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="exhibit-layout flex flex-col h-full w-full">
|
||||
<!-- <div class="exhibit-layout flex flex-col h-full w-full"> -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
@@ -27,8 +27,7 @@ export async function load({ params, parent }) {
|
||||
|
||||
events_func.load_ae_obj_li__exhibit_tracking({
|
||||
api_cfg: ae_acct.api,
|
||||
for_obj_type: 'event_exhibit',
|
||||
for_obj_id: exhibit_id,
|
||||
exhibit_id: exhibit_id,
|
||||
limit: 250,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
@@ -10,9 +10,22 @@
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { page } from '$app/state';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import { LoaderCircle, UserPlus, Download } from 'lucide-svelte';
|
||||
import {
|
||||
LoaderCircle,
|
||||
UserPlus,
|
||||
Download,
|
||||
Settings,
|
||||
Plus,
|
||||
List as ListIcon,
|
||||
LogIn,
|
||||
LayoutGrid,
|
||||
Search
|
||||
} from 'lucide-svelte';
|
||||
import Comp_exhibit_tracking_search from './ae_comp__exhibit_tracking_search.svelte';
|
||||
import Comp_exhibit_tracking_obj_li from './ae_comp__exhibit_tracking_obj_li.svelte';
|
||||
import Tab_add from './ae_tab__add.svelte';
|
||||
import Tab_start from './ae_tab__start.svelte';
|
||||
import Tab_manage from './ae_tab__manage.svelte';
|
||||
|
||||
// *** Initialization & Store Guard ***
|
||||
if ($events_loc.leads) {
|
||||
@@ -28,6 +41,13 @@
|
||||
$events_loc.leads.tracking__qry__sort_order = 'created_desc';
|
||||
}
|
||||
|
||||
// --- Tab State ---
|
||||
let active_tab = $state('list'); // 'start', 'add', 'list', 'manage'
|
||||
let previous_main_tab = $state('list'); // To remember if we were on 'add' or 'list' before going to 'manage'
|
||||
|
||||
// Mock sign-in state for now
|
||||
let is_signed_in = $state(true);
|
||||
|
||||
let tracking_id_li: Array<string> = $state([]);
|
||||
let search_debounce_timer: any = null;
|
||||
let last_search_id = 0;
|
||||
@@ -234,47 +254,110 @@
|
||||
log_lvl: 1
|
||||
});
|
||||
}
|
||||
|
||||
function toggle_main_tab() {
|
||||
if (active_tab === 'add') {
|
||||
active_tab = 'list';
|
||||
previous_main_tab = 'list';
|
||||
} else {
|
||||
active_tab = 'add';
|
||||
previous_main_tab = 'add';
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_manage_tab() {
|
||||
if (active_tab === 'manage') {
|
||||
active_tab = previous_main_tab;
|
||||
} else {
|
||||
active_tab = 'manage';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="ae_events_leads_tracking_new h-full w-full flex flex-col items-center space-y-4 p-4"
|
||||
class="ae_events_leads_tracking_new h-full min-w-lg md:min-w-md w-full flex flex-col items-center justify-center overflow-x-hidden outline-2 outline-red-600"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-6xl flex flex-col md:flex-row justify-between items-center gap-4"
|
||||
>
|
||||
<div class="text-center md:text-left">
|
||||
<h1 class="h2">
|
||||
Leads for {$lq__exhibit_obj?.name ?? 'Exhibitor'}
|
||||
<!-- Header -->
|
||||
<header class="grow-w w-full bg-surface-100-900 border-b border-surface-500/20 px-4 py-2 sticky top-0 z-10 flex items-center justify-between gap-4 shadow-sm">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<h1 class="text-base sm:text-lg font-bold truncate leading-tight">
|
||||
{$lq__exhibit_obj?.name ?? 'Exhibitor'}
|
||||
</h1>
|
||||
<p class="opacity-50">Booth #{$lq__exhibit_obj?.code ?? '...'}</p>
|
||||
<p class="text-[10px] sm:text-xs opacity-60">Booth #{$lq__exhibit_obj?.code ?? '...'}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
|
||||
<div class="flex items-center gap-1 sm:gap-2">
|
||||
<!-- Add Lead / Lead List Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-secondary"
|
||||
onclick={handle_export}
|
||||
class="btn btn-sm variant-filled-primary font-bold shadow-sm px-2 sm:px-4"
|
||||
onclick={toggle_main_tab}
|
||||
>
|
||||
<Download size="1.25em" class="mr-2" /> Export CSV
|
||||
{#if active_tab === 'add'}
|
||||
<ListIcon size="1.25em" class="sm:mr-2" />
|
||||
<span class="hidden sm:inline">Lead List</span>
|
||||
{:else}
|
||||
<Plus size="1.25em" class="sm:mr-2" />
|
||||
<span class="hidden sm:inline">Add Lead</span>
|
||||
{/if}
|
||||
</button>
|
||||
<a
|
||||
href={`/events/${page.params.event_id}/leads/exhibit/${page.params.exhibit_id}/scan`}
|
||||
class="btn preset-filled-primary"
|
||||
|
||||
<!-- Manage / Config -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm transition-colors px-2 sm:px-3"
|
||||
class:variant-filled-surface={active_tab === 'manage'}
|
||||
class:variant-ghost-surface={active_tab !== 'manage'}
|
||||
onclick={toggle_manage_tab}
|
||||
title="Manage Exhibit"
|
||||
>
|
||||
<UserPlus size="1.25em" class="mr-2" /> Add Lead
|
||||
</a>
|
||||
<Settings size="1.25em" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area - Stable Width -->
|
||||
<div class="w-full flex-1 flex flex-col items-center">
|
||||
<div class="w-full px-4 sm:px-6 py-6 space-y-6">
|
||||
{#if !is_signed_in}
|
||||
<div class="w-full max-w-4xl mx-auto">
|
||||
<Tab_start />
|
||||
</div>
|
||||
{:else if active_tab === 'add'}
|
||||
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
{:else if active_tab === 'list'}
|
||||
<div class="w-full flex flex-col space-y-6">
|
||||
<div class="flex justify-between items-center px-2">
|
||||
<h2 class="text-xl sm:text-2xl font-bold">Lead List</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-ghost-secondary"
|
||||
onclick={handle_export}
|
||||
>
|
||||
<Download size="1.2em" class="mr-2" /> Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Comp_exhibit_tracking_search exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
|
||||
{#if $events_sess.leads.submit_status__search === 'searching' && tracking_id_li.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-10 opacity-50 text-center w-full"
|
||||
>
|
||||
<LoaderCircle size="3em" class="animate-spin mb-4 mx-auto" />
|
||||
<p class="text-xl">Searching leads...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Comp_exhibit_tracking_obj_li {lq__event_exhibit_tracking_obj_li} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if active_tab === 'manage'}
|
||||
<div class="w-full max-w-4xl mx-auto">
|
||||
<Tab_manage />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Comp_exhibit_tracking_search exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
|
||||
{#if $events_sess.leads.submit_status__search === 'searching' && tracking_id_li.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-10 opacity-50 text-center"
|
||||
>
|
||||
<LoaderCircle size="3em" class="animate-spin mb-4 mx-auto" />
|
||||
<p class="text-xl">Searching leads...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Comp_exhibit_tracking_obj_li {lq__event_exhibit_tracking_obj_li} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ae_comp__exhibit_tracking_obj_li w-full max-w-6xl mx-auto px-4">
|
||||
<div class="ae_comp__exhibit_tracking_obj_li w-full px-2 sm:px-4">
|
||||
{#if !$lq__event_exhibit_tracking_obj_li}
|
||||
<div class="flex justify-center p-10">
|
||||
<span class="fas fa-spinner fa-spin fa-2x opacity-20"></span>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
handle_search_trigger();
|
||||
})}
|
||||
autocomplete="off"
|
||||
class="search_form flex flex-row flex-wrap gap-1 items-center justify-center w-full max-w-7xl px-2 md:px-12 py-2 preset-tonal-primary rounded-lg shadow-sm"
|
||||
class="search_form flex flex-row flex-wrap gap-1 items-center justify-center w-full px-2 py-2 preset-tonal-primary rounded-lg shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-center justify-center gap-1 grow"
|
||||
|
||||
@@ -1,11 +1,118 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_manual_search.svelte
|
||||
* Manual Attendee Search Stub.
|
||||
* Manual Attendee Search for adding leads.
|
||||
*/
|
||||
import { page } from '$app/state';
|
||||
import { ae_api } from '$lib/stores/ae_stores';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import { Search, UserPlus, CheckCircle, LoaderCircle } from 'lucide-svelte';
|
||||
import type { ae_EventBadge } from '$lib/types/ae_types';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
|
||||
interface Props {
|
||||
exhibit_id: string;
|
||||
on_lead_added?: (badge: ae_EventBadge) => void;
|
||||
}
|
||||
|
||||
let { exhibit_id, on_lead_added }: Props = $props();
|
||||
|
||||
let search_query = $state('');
|
||||
let results: ae_EventBadge[] = $state([]);
|
||||
let searching = $state(false);
|
||||
let adding_id = $state('');
|
||||
|
||||
async function handle_search() {
|
||||
if (!search_query.trim()) return;
|
||||
searching = true;
|
||||
try {
|
||||
const search_results = await events_func.search__event_badge({
|
||||
api_cfg: $ae_api,
|
||||
event_id: page.params.event_id || '',
|
||||
fulltext_search_qry_str: search_query,
|
||||
limit: 20
|
||||
});
|
||||
results = Array.isArray(search_results) ? search_results : [];
|
||||
} catch (e) {
|
||||
console.error('Badge search failed', e);
|
||||
} finally {
|
||||
searching = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function add_as_lead(badge: ae_EventBadge) {
|
||||
if (!badge.event_badge_id_random) return;
|
||||
adding_id = badge.event_badge_id_random;
|
||||
|
||||
// TODO: Get the actual signed-in licensed user's email
|
||||
const user_email = 'placeholder@exhibitor.com';
|
||||
|
||||
try {
|
||||
const result = await events_func.create_ae_obj__exhibit_tracking({
|
||||
api_cfg: $ae_api,
|
||||
exhibit_id: exhibit_id,
|
||||
event_badge_id: badge.event_badge_id_random,
|
||||
external_person_id: user_email
|
||||
});
|
||||
|
||||
if (result && on_lead_added) {
|
||||
on_lead_added(badge);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to add lead', e);
|
||||
} finally {
|
||||
adding_id = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lead-manual-search p-4 card">
|
||||
<h3 class="h3">Manual Search</h3>
|
||||
<p>Placeholder for attendee lookup.</p>
|
||||
</div>
|
||||
<div class="lead-manual-search space-y-4 w-full">
|
||||
<form
|
||||
class="flex gap-2"
|
||||
onsubmit={(e) => { e.preventDefault(); handle_search(); }}
|
||||
>
|
||||
<div class="relative grow">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 opacity-50" size="1.2em" />
|
||||
<input
|
||||
type="search"
|
||||
bind:value={search_query}
|
||||
placeholder="Name, email, or badge ID..."
|
||||
class="input pl-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn preset-filled-primary" disabled={searching}>
|
||||
{#if searching}
|
||||
<LoaderCircle class="animate-spin mr-2" size="1.2em" />
|
||||
{/if}
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{#if results.length > 0}
|
||||
<div class="results-list space-y-2 max-h-[50vh] overflow-y-auto pr-2">
|
||||
{#each results as badge}
|
||||
<div class="card p-3 flex justify-between items-center variant-soft shadow-sm">
|
||||
<div>
|
||||
<div class="font-bold">{badge.full_name}</div>
|
||||
<div class="text-xs opacity-70">{badge.affiliations || badge.email || ''}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-success"
|
||||
disabled={adding_id === badge.event_badge_id_random}
|
||||
onclick={() => add_as_lead(badge)}
|
||||
>
|
||||
{#if adding_id === badge.event_badge_id_random}
|
||||
<LoaderCircle class="animate-spin" size="1em" />
|
||||
{:else}
|
||||
<UserPlus size="1em" class="mr-1" />
|
||||
{/if}
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !searching && search_query}
|
||||
<p class="text-center opacity-50 py-4 italic">No attendees found matching "{search_query}"</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,11 +1,147 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__lead_qr_scanner.svelte
|
||||
* Badge QR Scanner Stub.
|
||||
* Badge QR Scanner for adding leads.
|
||||
*/
|
||||
import { page } from '$app/state';
|
||||
import { ae_api } from '$lib/stores/ae_stores';
|
||||
import { events_func } from '$lib/ae_events_functions';
|
||||
import Element_qr_scanner_v2 from '$lib/element_qr_scanner_v2.svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { LoaderCircle, UserPlus, CheckCircle, AlertCircle } from 'lucide-svelte';
|
||||
import type { ae_EventBadge } from '$lib/types/ae_types';
|
||||
|
||||
interface Props {
|
||||
exhibit_id: string;
|
||||
on_lead_added?: (badge: ae_EventBadge) => void;
|
||||
}
|
||||
|
||||
let { exhibit_id, on_lead_added }: Props = $props();
|
||||
|
||||
let start_qr_scanner = $state(true);
|
||||
let scanning_status = $state('idle'); // idle, scanning, found, adding, success, error
|
||||
let found_badge: ae_EventBadge | null = $state(null);
|
||||
let error_msg = $state('');
|
||||
|
||||
async function handle_qr_scan_result(event: CustomEvent) {
|
||||
const qr_result = event.detail.result;
|
||||
const obj = ae_util.process_data_string(qr_result);
|
||||
|
||||
if (obj && obj.type === 'event_badge' && obj.id) {
|
||||
scanning_status = 'found';
|
||||
start_qr_scanner = false;
|
||||
|
||||
// Load full badge info
|
||||
try {
|
||||
found_badge = await events_func.load_ae_obj_id__event_badge({
|
||||
api_cfg: $ae_api,
|
||||
event_badge_id: obj.id,
|
||||
log_lvl: 1
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load badge info', e);
|
||||
}
|
||||
} else {
|
||||
scanning_status = 'error';
|
||||
error_msg = 'Invalid QR code. Please scan an Event Badge.';
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm_add_lead() {
|
||||
if (!found_badge || !found_badge.event_badge_id_random) return;
|
||||
|
||||
scanning_status = 'adding';
|
||||
const user_email = 'placeholder@exhibitor.com';
|
||||
|
||||
try {
|
||||
const result = await events_func.create_ae_obj__exhibit_tracking({
|
||||
api_cfg: $ae_api,
|
||||
exhibit_id: exhibit_id,
|
||||
event_badge_id: found_badge.event_badge_id_random,
|
||||
external_person_id: user_email
|
||||
});
|
||||
|
||||
if (result) {
|
||||
scanning_status = 'success';
|
||||
if (on_lead_added) on_lead_added(found_badge);
|
||||
|
||||
// Auto-reset after 2 seconds to scan next
|
||||
setTimeout(reset_scanner, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
scanning_status = 'error';
|
||||
error_msg = 'Failed to add lead. They might already be added.';
|
||||
}
|
||||
}
|
||||
|
||||
function reset_scanner() {
|
||||
scanning_status = 'idle';
|
||||
found_badge = null;
|
||||
error_msg = '';
|
||||
start_qr_scanner = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lead-qr-scanner p-4 card">
|
||||
<h3 class="h3">Scan Badge</h3>
|
||||
<p>Placeholder for QR scanner component.</p>
|
||||
</div>
|
||||
<div class="lead-qr-scanner flex flex-col items-center space-y-4 w-full min-h-[400px] justify-center">
|
||||
{#if scanning_status === 'idle' || scanning_status === 'scanning'}
|
||||
<div class="w-full max-w-sm mx-auto aspect-square overflow-hidden rounded-xl border-4 border-surface-500/20 shadow-xl relative bg-surface-900/10">
|
||||
<Element_qr_scanner_v2
|
||||
bind:start_qr_scanner
|
||||
on:qr_scan_result={handle_qr_scan_result}
|
||||
/>
|
||||
<div class="absolute inset-0 pointer-events-none border-2 border-primary-500/50 m-8 sm:m-12 rounded-lg animate-pulse"></div>
|
||||
</div>
|
||||
<p class="text-center opacity-70 italic text-sm">Point camera at the badge QR code</p>
|
||||
|
||||
{:else if scanning_status === 'found' || scanning_status === 'adding'}
|
||||
<div class="card p-6 w-full max-w-md space-y-4 variant-soft-primary shadow-xl border-2 border-primary-500">
|
||||
<div class="text-center">
|
||||
<h3 class="h3 font-bold">{found_badge?.full_name || 'Badge Found'}</h3>
|
||||
<p class="opacity-70">{found_badge?.affiliations || ''}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xl w-full preset-filled-primary font-bold py-6"
|
||||
disabled={scanning_status === 'adding'}
|
||||
onclick={confirm_add_lead}
|
||||
>
|
||||
{#if scanning_status === 'adding'}
|
||||
<LoaderCircle class="animate-spin mr-2" size="1.5em" />
|
||||
Adding Lead...
|
||||
{:else}
|
||||
<UserPlus size="1.5em" class="mr-2" />
|
||||
Add as Lead
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm w-full opacity-50"
|
||||
disabled={scanning_status === 'adding'}
|
||||
onclick={reset_scanner}
|
||||
>
|
||||
Cancel / Scan Again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if scanning_status === 'success'}
|
||||
<div class="card p-10 w-full max-w-md flex flex-col items-center space-y-4 variant-soft-success shadow-xl">
|
||||
<CheckCircle size="4em" class="text-success-500 animate-bounce" />
|
||||
<div class="text-center">
|
||||
<h3 class="h4 font-bold">Lead Added!</h3>
|
||||
<p class="text-xl font-bold">{found_badge?.full_name}</p>
|
||||
</div>
|
||||
<p class="text-sm opacity-50">Resetting scanner...</p>
|
||||
</div>
|
||||
|
||||
{:else if scanning_status === 'error'}
|
||||
<div class="card p-6 w-full max-w-md flex flex-col items-center space-y-4 variant-soft-error">
|
||||
<AlertCircle size="3em" class="text-error-500" />
|
||||
<p class="text-center font-bold">{error_msg}</p>
|
||||
<button type="button" class="btn btn-sm preset-filled-error" onclick={reset_scanner}>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,18 +1,72 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__add.svelte
|
||||
* Tab 2: Add - Search / QR Scan Stub.
|
||||
* Tab 2: Add - Search / QR Scan Layout.
|
||||
*/
|
||||
import { QrCode, Search, List } from 'lucide-svelte';
|
||||
import Comp_lead_qr_scanner from './ae_comp__lead_qr_scanner.svelte';
|
||||
import Comp_lead_manual_search from './ae_comp__lead_manual_search.svelte';
|
||||
import { events_loc } from '$lib/stores/ae_events_stores';
|
||||
|
||||
let show_qr = $state(true);
|
||||
interface Props {
|
||||
exhibit_id: string;
|
||||
}
|
||||
|
||||
let { exhibit_id }: Props = $props();
|
||||
|
||||
let mode = $state('qr'); // 'qr' or 'search'
|
||||
|
||||
function handle_lead_added(badge: any) {
|
||||
console.log('Lead successfully added:', badge.full_name);
|
||||
// We could trigger a global list refresh here if needed
|
||||
$events_loc.leads.tracking__search_version++;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ae-tab-add p-4 space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn btn-sm" onclick={() => show_qr = true}>QR</button>
|
||||
<button type="button" class="btn btn-sm" onclick={() => show_qr = false}>Search</button>
|
||||
<div class="ae-tab-add flex flex-col items-center space-y-6 w-full mx-auto">
|
||||
<!-- Mode Toggle - Stable Width -->
|
||||
<div class="flex p-1 bg-surface-200-800 rounded-xl w-full max-w-md shadow-inner">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 btn btn-sm py-3 flex items-center justify-center gap-2 rounded-lg transition-all duration-200"
|
||||
class:preset-filled-primary={mode === 'qr'}
|
||||
onclick={() => mode = 'qr'}
|
||||
>
|
||||
<QrCode size="1.2em" />
|
||||
<span class="font-bold">Scan QR</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 btn btn-sm py-3 flex items-center justify-center gap-2 rounded-lg transition-all duration-200"
|
||||
class:preset-filled-primary={mode === 'search'}
|
||||
onclick={() => mode = 'search'}
|
||||
>
|
||||
<Search size="1.2em" />
|
||||
<span class="font-bold">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if show_qr} <Comp_lead_qr_scanner /> {:else} <Comp_lead_manual_search /> {/if}
|
||||
</div>
|
||||
|
||||
<!-- Content Area - Stable Width -->
|
||||
<div class="w-full flex flex-col items-center min-h-[400px]">
|
||||
{#if mode === 'qr'}
|
||||
<Comp_lead_qr_scanner {exhibit_id} on_lead_added={handle_lead_added} />
|
||||
{:else}
|
||||
<Comp_lead_manual_search {exhibit_id} on_lead_added={handle_lead_added} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quick Navigation -->
|
||||
<div class="pt-8 w-full border-t border-surface-500/20 flex justify-center pb-10">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-surface opacity-70 hover:opacity-100 transition-opacity"
|
||||
onclick={() => {
|
||||
// This would be handled by the parent's active_tab state
|
||||
// Assuming we can pass a prop or use a store to switch tabs
|
||||
}}
|
||||
>
|
||||
<List size="1.2em" class="mr-2" />
|
||||
View Leads List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,7 +281,7 @@ Middle-click to open in new tab`}
|
||||
);
|
||||
}
|
||||
$journals_slct.journal_entry_id =
|
||||
results?.journal_entry_id_random;
|
||||
results?.journal_entry_id;
|
||||
// $journals_loc.entry.edit = true;
|
||||
$journals_loc.entry.edit_kv[
|
||||
$journals_slct.journal_entry_id
|
||||
|
||||
@@ -1,74 +1,8 @@
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
import { error } from '@sveltejs/kit';
|
||||
console.log(`ae_p_journals [journal_id] +page.ts start`);
|
||||
// console.log(`ae_p_journals [journal_id] +page.ts start`);
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
// import { browser } from '$app/environment';
|
||||
// import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
|
||||
export async function load({ params, parent }) {
|
||||
// route
|
||||
// let log_lvl: number = 1;
|
||||
// let data = await parent();
|
||||
// data.log_lvl = log_lvl;
|
||||
// let account_id = data.account_id;
|
||||
// let ae_acct = data[account_id];
|
||||
// let journal_id = params.journal_id;
|
||||
// if (!journal_id) {
|
||||
// console.log(`ae_journals journals [journal_id] +page.ts: The journal_id was not found in the params!!!`);
|
||||
// error(404, {
|
||||
// message: 'Journals - Journal ID not found'
|
||||
// });
|
||||
// }
|
||||
// ae_acct.slct.journal_id = journal_id;
|
||||
// console.log(`ae_journals journals [journal_id] +page.ts: journal_id = `, ae_acct.slct.journal_id);
|
||||
// if (browser) {
|
||||
// if (log_lvl) {
|
||||
// console.log(`ae_journals journals [journal_id] +page.ts: journal_id = `, journal_id);
|
||||
// }
|
||||
// // Load event journal object
|
||||
// let load_journal_obj = journals_func.load_ae_obj_id__journal({
|
||||
// api_cfg: ae_acct.api,
|
||||
// journal_id: journal_id,
|
||||
// inc_entry_li: true,
|
||||
// try_cache: true,
|
||||
// log_lvl: log_lvl
|
||||
// });
|
||||
// ae_acct.slct.journal_obj = load_journal_obj;
|
||||
// Load journal entries for the journal
|
||||
// let load_journal_entry_obj_li = journals_func.load_ae_obj_li__journal_entry({
|
||||
// api_cfg: ae_acct.api,
|
||||
// for_obj_type: 'journal',
|
||||
// for_obj_id: journal_id,
|
||||
// params: {qry__enabled: 'all', qry__limit: 99},
|
||||
// try_cache: true
|
||||
// })
|
||||
// .then((journal_entry_obj_li) => {
|
||||
// if (log_lvl) {
|
||||
// console.log(`journal_entry_obj_li = `, journal_entry_obj_li);
|
||||
// }
|
||||
// for (let index = 0; index < journal_entry_obj_li.length; index++) {
|
||||
// let journal_entry_obj = journal_entry_obj_li[index];
|
||||
// let journal_entry_id = journal_entry_obj.journal_entry_id_random;
|
||||
// let load_journal_entry_obj_li = journals_func.load_ae_obj_li__journal_entry({
|
||||
// api_cfg: ae_acct.api,
|
||||
// for_obj_type: 'journal_entry',
|
||||
// for_obj_id: journal_entry_id,
|
||||
// params: {qry__enabled: 'all', qry__limit: 15},
|
||||
// try_cache: true
|
||||
// });
|
||||
// if (log_lvl) {
|
||||
// console.log(`load_journal_entry_obj_li = `, load_journal_entry_obj_li);
|
||||
// }
|
||||
// journal_entry_obj_li[index].journal_entry_li = load_journal_entry_obj_li;
|
||||
// }
|
||||
// return journal_entry_obj_li;
|
||||
// });
|
||||
// if (log_lvl) {
|
||||
// console.log(`load_journal_entry_obj_li = `, load_journal_entry_obj_li);
|
||||
// }
|
||||
// ae_acct.slct.journal_entry_obj_li = load_journal_entry_obj_li;
|
||||
// }
|
||||
// WARNING: Precaution against shared data between sites.
|
||||
// data[account_id] = ae_acct;
|
||||
// return data;
|
||||
}
|
||||
// export async function load({ params, parent }) {
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user