feat: leads re-enable flow — detect removed leads on scan + Remove/Restore buttons

- QR scanner (single + multi): detect previously-removed leads via IDB enable flag;
  route to 'reenable' state instead of duplicate error; offer Re-activate button
- API fallback: if create fails and no IDB record, search API for disabled tracking
  record by event_exhibit_id + event_badge_id (adds qry_badge_id param to
  search__exhibit_tracking)
- Lead detail page: Replace raw enable checkbox with Remove Lead (two-click confirm,
  navigates back after) and Restore Lead card (shown when enable is falsy)
- Fix flash of disabled records in leads list: filter !enable in both filtered_lead_li
  derived and local IDB fast-path in handle_search_refresh
- eslint.config.js: disable svelte/no-navigation-without-resolve (no base path configured)
- Also includes _random field annotation cleanup (db_events, ae_types), iframe layout
  fixes, badge view tweaks, test updates, and doc updates from prior session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-20 18:18:10 -04:00
parent 4586e809d7
commit 6662e82f40
18 changed files with 505 additions and 266 deletions

View File

@@ -535,8 +535,8 @@ Button has `data-testid="badge-print-btn"` and shows loading/done/error states w
**Indexed Fields:**
```typescript
badge: `
event_badge_id_random, event_badge_id, id,
event_id, event_id_random,
event_badge_id, id,
event_id,
full_name, full_name_override, email, email_override,
affiliations, affiliations_override,
badge_type, badge_type_code, badge_type_code_override, badge_type_override,

View File

@@ -132,7 +132,7 @@ One exhibitor's presence at an event.
| Field | Purpose |
|---|---|
| `event_exhibit_id` / `event_exhibit_id_random` | Primary / URL-safe ID |
| `event_exhibit_id` | Primary / URL-safe ID |
| `name` | Exhibitor display name |
| `code` | Booth number |
| `staff_passcode` | Shared sign-in code |
@@ -232,4 +232,4 @@ Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhi
- `license_max` controls how many licensed staff slots an exhibit can have
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id_random]/`
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id]/`

View File

@@ -106,7 +106,7 @@ V3 returns detailed error metadata in the `meta.details` object.
**Symptom:** Providing a string ID in a search body that the backend maps to an integer can result in **Zero Results** if the underlying view expects a string.
**Final Solution (Body + Header Injection):**
1. **Body:** Inject the raw field name (e.g. `account_id_random`) into the `search_query.and` array to bypass automatic backend mapping.
1. **Body:** Inject the raw field name (e.g. `account_id`) into the `search_query.and` array to bypass automatic backend mapping.
2. **Headers:** Pass `headers: { 'x-account-id': ... }` manually to provide context for Auth validation.
3. **Isolation (IDAA):** Due to specific bugs in the IDAA module, it has been temporarily isolated to a legacy V2 search function (`qry_ae_obj_li__event_v2`) using `default_qry_str` for text searching, while the main module continues to use the V3 implementation.

View File

@@ -32,7 +32,9 @@ export default tseslint.config(
},
{
rules: {
'@typescript-eslint/no-unused-vars': 'warn'
'@typescript-eslint/no-unused-vars': 'warn',
// No base path configured — this rule is not applicable to this project
'svelte/no-navigation-without-resolve': 'off'
}
}
);

View File

@@ -458,6 +458,7 @@ export async function search__exhibit_tracking({
fulltext_search_qry_str = null,
qry_group = null,
qry_external_person_id = null,
qry_badge_id = null,
enabled = 'enabled',
hidden = 'all',
view = 'default',
@@ -473,6 +474,7 @@ export async function search__exhibit_tracking({
fulltext_search_qry_str?: string | null;
qry_group?: string | null;
qry_external_person_id?: string | null;
qry_badge_id?: string | null;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
view?: string;
@@ -498,6 +500,7 @@ export async function search__exhibit_tracking({
if (qry_group) search_query.and.push({ field: 'group', op: 'eq', value: qry_group });
if (qry_external_person_id) search_query.and.push({ field: 'external_person_id', op: 'eq', value: qry_external_person_id });
if (qry_badge_id) search_query.and.push({ field: 'event_badge_id', op: 'eq', value: qry_badge_id });
if (enabled === 'enabled') search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
else if (enabled === 'not_enabled') search_query.and.push({ field: 'enable', op: 'eq', value: 0 });

View File

@@ -16,12 +16,12 @@ export interface Event {
id: string;
// id_random: string;
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
code: string;
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
conference: boolean;
type: string;
@@ -114,7 +114,7 @@ export interface Badge {
id: string;
// id_random: string;
event_badge_id: string;
event_badge_id_random: string;
event_badge_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
@@ -210,10 +210,10 @@ export interface Badge_template {
// id_random: string;
event_badge_template_id?: null | string;
event_badge_template_id_random?: null | string;
event_badge_template_id_random?: null | string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
name: string;
description?: null | string;
@@ -274,14 +274,14 @@ export interface Badge_template {
// Updated 2024-10-16
export interface Device {
id: string;
// id_random: string;
// id_random: string; // NO LONGER USE "_random"
event_device_id: string;
// event_device_id_random: string;
// event_device_id_random: string; // NO LONGER USE "_random"
event_id: string;
// event_id_random: string;
// event_id_random: string; // NO LONGER USE "_random"
event_location_id?: string;
// event_location_id_random?: string;
// event_location_id_random?: string; // NO LONGER USE "_random"
code?: string;
name: string;
@@ -342,12 +342,12 @@ export interface Device {
export interface Exhibit {
id: string;
id_random: string;
id_random: string; // NO LONGER USE "_random"
event_exhibit_id: string;
event_exhibit_id_random: string;
event_exhibit_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
code: string;
name: string;
@@ -380,16 +380,16 @@ export interface Exhibit {
export interface Exhibit_tracking {
id: string;
id_random: string;
id_random: string; // NO LONGER USE "_random"
event_exhibit_tracking_id: string;
event_exhibit_tracking_id_random: string;
event_exhibit_tracking_id_random: string; // NO LONGER USE "_random"
event_exhibit_id: string;
event_exhibit_id_random: string;
event_exhibit_id_random: string; // NO LONGER USE "_random"
event_badge_id: string;
event_badge_id_random: string;
event_badge_id_random: string; // NO LONGER USE "_random"
event_person_id: string;
event_person_id_random?: string;
event_person_id_random?: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
@@ -442,28 +442,28 @@ export interface Exhibit_tracking {
export interface EventFile {
id: string;
id_random: string;
id_random: string; // NO LONGER USE "_random"
event_file_id: string;
event_file_id_random?: string;
event_file_id_random?: string; // NO LONGER USE "_random"
hosted_file_id: string;
hosted_file_id_random?: string;
hosted_file_id_random?: string; // NO LONGER USE "_random"
hash_sha256: string;
for_type?: string;
for_id?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
event_id: string;
event_id_random?: string;
event_id_random?: string; // NO LONGER USE "_random"
event_session_id?: string;
event_session_id_random?: string;
event_session_id_random?: string; // NO LONGER USE "_random"
event_presentation_id?: string;
event_presentation_id_random?: string;
event_presentation_id_random?: string; // NO LONGER USE "_random"
event_presenter_id?: string;
event_presenter_id_random?: string;
event_presenter_id_random?: string; // NO LONGER USE "_random"
event_location_id?: string;
event_location_id_random?: string;
event_location_id_random?: string; // NO LONGER USE "_random"
filename: string;
extension: string;
@@ -517,7 +517,7 @@ export interface Location {
id: string;
// id_random: string;
event_location_id: string;
event_location_id_random: string;
event_location_id_random: string; // NO LONGER USE "_random"
external_id?: null | string;
code?: null | string;
@@ -525,7 +525,7 @@ export interface Location {
type_code?: string;
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
name: string;
description?: null | string;
@@ -571,23 +571,23 @@ export interface Presentation {
id: string;
// id_random: string;
event_presentation_id: string;
event_presentation_id_random: string;
event_presentation_id_random: string; // NO LONGER USE "_random"
external_id?: null | string;
code?: null | string;
for_type?: string;
for_id?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
type_code?: string;
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_session_id: string;
event_session_id_random: string;
event_session_id_random: string; // NO LONGER USE "_random"
event_abstract_id?: null | string;
event_abstract_id_random?: null | string;
event_abstract_id_random?: null | string; // NO LONGER USE "_random"
abstract_code?: null | string;
@@ -630,23 +630,23 @@ export interface Presenter {
id: string;
// id_random: string;
event_presenter_id: string;
event_presenter_id_random: string;
event_presenter_id_random: string; // NO LONGER USE "_random"
external_id?: string;
code?: string;
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_session_id: string;
event_session_id_random: string;
event_session_id_random: string; // NO LONGER USE "_random"
event_person_id?: null | string;
event_person_id_random?: null | string;
event_person_id_random?: null | string; // NO LONGER USE "_random"
event_presentation_id: string;
event_presentation_id_random: string;
event_presentation_id_random: string; // NO LONGER USE "_random"
person_id?: null | string;
person_id_random?: null | string;
person_profile_id?: null | string;
person_profile_id_random?: null | string; // The new table person_profile will be used soon...
person_id_random?: null | string; // NO LONGER USE "_random"
person_profile_id?: null | string; // The new table person_profile will be used soon...
person_profile_id_random?: null | string; // NO LONGER USE "_random"
pronouns?: null | string;
informal_name?: null | string;
@@ -731,24 +731,24 @@ export interface Session {
id: string;
// id_random: string;
event_session_id: string;
event_session_id_random: string;
event_session_id_random: string; // NO LONGER USE "_random"
external_id: null | string;
code: null | string;
for_type: string;
for_id: string;
for_id_random: string;
for_id_random: string; // NO LONGER USE "_random"
type_code?: string;
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_location_id?: null | string;
event_location_id_random?: null | string;
event_location_id_random?: null | string; // NO LONGER USE "_random"
poc_person_id?: null | string;
poc_person_id_random?: null | string;
poc_person_id_random?: null | string; // NO LONGER USE "_random"
poc_agree?: null | boolean; // General catchall for agreement or consent by the POC
poc_kv_json?: null | key_val; // Key value list of the POC by type (examples: 'advocate', 'chair', 'champion', 'moderator', 'organizer')
@@ -829,6 +829,7 @@ export class MySubClassedDexie extends Dexie {
constructor() {
super('ae_events_db');
this.version(6).stores({
// NO LONGER USE "_random"
event: `
id, event_id, event_id_random,
code,
@@ -841,7 +842,6 @@ export class MySubClassedDexie extends Dexie {
tmp_sort_1, tmp_sort_2,
enable, hide, priority, sort, group, notes, created_on, updated_on`,
// badge: '++id, full_name, email' // Primary key and indexed props
badge: `
event_badge_id_random, event_badge_id, id,
event_id, event_id_random,

View File

@@ -42,7 +42,7 @@ export interface ae_BaseObj {
*/
export interface ae_Account extends ae_BaseObj {
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
account_cfg?: ae_AccountCfg;
}
@@ -52,7 +52,7 @@ export interface ae_Account extends ae_BaseObj {
*/
export interface ae_AccountCfg {
id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
modules_enabled?: any;
default_no_reply_email?: string;
@@ -70,9 +70,9 @@ export interface ae_AccountCfg {
*/
export interface ae_Site extends ae_BaseObj {
site_id: string;
site_id_random: string;
site_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
url_root?: string;
site_cfg_json?: any;
@@ -94,11 +94,11 @@ export interface ae_Site extends ae_BaseObj {
*/
export interface ae_SiteDomain extends ae_BaseObj {
site_domain_id: string;
site_domain_id_random: string;
site_domain_id_random: string; // NO LONGER USE "_random"
site_id: string;
site_id_random: string;
site_id_random: string; // NO LONGER USE "_random"
account_id?: string;
account_id_random?: string;
account_id_random?: string; // NO LONGER USE "_random"
fqdn: string;
is_primary?: boolean;
@@ -125,15 +125,15 @@ export interface ae_SiteDomain extends ae_BaseObj {
*/
export interface ae_Journal extends ae_BaseObj {
journal_id: string;
journal_id_random: string;
journal_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
person_id?: string;
person_id_random?: string;
person_id_random?: string; // NO LONGER USE "_random"
for_type?: string;
for_id?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
type_code?: string;
tags?: string;
@@ -163,12 +163,12 @@ export interface ae_Journal extends ae_BaseObj {
*/
export interface ae_JournalEntry extends ae_BaseObj {
journal_entry_id: string;
// journal_entry_id_random: string;
// journal_entry_id_random: string; // NO LONGER USE "_random"
journal_id: string;
// journal_id_random: string;
// journal_id_random: string; // NO LONGER USE "_random"
person_id?: string;
person_id_random?: string;
person_id_random?: string; // NO LONGER USE "_random"
template?: boolean;
@@ -235,12 +235,12 @@ export interface ae_JournalEntry extends ae_BaseObj {
*/
export interface ae_Person extends ae_BaseObj {
person_id: string;
person_id_random: string;
person_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
user_id?: string;
user_id_random?: string;
user_id_random?: string; // NO LONGER USE "_random"
prefix?: string;
title_names?: string;
@@ -282,11 +282,11 @@ export interface ae_Person extends ae_BaseObj {
*/
export interface ae_User extends ae_BaseObj {
user_id: string;
user_id_random: string;
user_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
person_id?: string;
person_id_random?: string;
person_id_random?: string; // NO LONGER USE "_random"
username: string;
email?: string;
@@ -309,10 +309,10 @@ export interface ae_User extends ae_BaseObj {
*/
export interface ae_UserRole {
id: string;
user_id_random: string;
user_id_random: string; // NO LONGER USE "_random"
for_type?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
code?: string;
name?: string;
@@ -327,12 +327,12 @@ export interface ae_UserRole {
*/
export interface ae_Address extends ae_BaseObj {
address_id: string;
address_id_random: string;
address_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
for_type?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
attention_to?: string;
organization_name?: string;
@@ -356,13 +356,13 @@ export interface ae_Address extends ae_BaseObj {
*/
export interface ae_Contact extends ae_BaseObj {
contact_id: string;
contact_id_random: string;
contact_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
address_id_random?: string;
address_id_random?: string; // NO LONGER USE "_random"
for_type?: string;
for_id_random?: string;
for_id_random?: string; // NO LONGER USE "_random"
title?: string;
tagline?: string;
@@ -382,9 +382,9 @@ export interface ae_Contact extends ae_BaseObj {
*/
export interface ae_ActivityLog extends ae_BaseObj {
activity_log_id: string;
activity_log_id_random: string;
activity_log_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
person_id_random?: string;
user_id_random?: string;
@@ -408,9 +408,9 @@ export interface ae_ActivityLog extends ae_BaseObj {
*/
export interface ae_Event extends ae_BaseObj {
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
conference: boolean;
type?: string;
@@ -453,7 +453,7 @@ export interface ae_Event extends ae_BaseObj {
* EventCfg - Advanced configuration for an event
*/
export interface ae_EventCfg extends ae_BaseObj {
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
enable_comments?: boolean;
unauthenticated_access?: boolean;
@@ -468,16 +468,16 @@ export interface ae_EventCfg extends ae_BaseObj {
*/
export interface ae_EventBadge extends ae_BaseObj {
event_badge_id: string;
event_badge_id_random: string;
event_badge_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
person_id: string;
person_id_random: string;
person_id_random: string; // NO LONGER USE "_random"
event_person_id?: string | null;
event_person_id_random?: string | null;
event_person_id_random?: string | null; // NO LONGER USE "_random"
event_badge_template_id?: string | null;
event_badge_template_id_random?: string | null;
event_badge_template_id_random?: string | null; // NO LONGER USE "_random"
badge_type_code?: string | null;
badge_type?: string | null;
@@ -516,9 +516,9 @@ export interface ae_EventBadge extends ae_BaseObj {
*/
export interface ae_EventBadgeTemplate extends ae_BaseObj {
event_badge_template_id: string;
event_badge_template_id_random: string;
event_badge_template_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
logo_path?: string | null;
header_path?: string | null;
@@ -541,9 +541,9 @@ export interface ae_EventBadgeTemplate extends ae_BaseObj {
*/
export interface ae_EventLocation extends ae_BaseObj {
event_location_id: string;
event_location_id_random: string;
event_location_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
location_type?: string | null;
location_type_code?: string | null;
@@ -561,13 +561,13 @@ export interface ae_EventLocation extends ae_BaseObj {
*/
export interface ae_EventSession extends ae_BaseObj {
event_session_id: string;
event_session_id_random: string;
event_session_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_location_id?: string | null;
event_location_id_random?: string | null;
event_location_id_random?: string | null; // NO LONGER USE "_random"
event_track_id?: string | null;
event_track_id_random?: string | null;
event_track_id_random?: string | null; // NO LONGER USE "_random"
type_code?: string | null;
start_datetime?: string | Date | null;
@@ -610,13 +610,13 @@ export interface ae_EventSession extends ae_BaseObj {
*/
export interface ae_EventPresentation extends ae_BaseObj {
event_presentation_id: string;
event_presentation_id_random: string;
event_presentation_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_session_id: string;
event_session_id_random: string;
event_session_id_random: string; // NO LONGER USE "_random"
event_abstract_id?: string | null;
event_abstract_id_random?: string | null;
event_abstract_id_random?: string | null; // NO LONGER USE "_random"
abstract_code?: string | null;
type_code?: string | null;
@@ -639,11 +639,11 @@ export interface ae_EventPresentation extends ae_BaseObj {
*/
export interface ae_EventPresenter extends ae_BaseObj {
event_presenter_id: string;
event_presenter_id_random: string;
event_presenter_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
person_id?: string | null;
person_id_random?: string | null;
person_id_random?: string | null; // NO LONGER USE "_random"
pronouns?: string | null;
informal_name?: string | null;
@@ -666,9 +666,9 @@ export interface ae_EventPresenter extends ae_BaseObj {
*/
export interface ae_EventTrack extends ae_BaseObj {
event_track_id: string;
event_track_id_random: string;
event_track_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
track_type?: string;
track_type_code?: string;
@@ -682,9 +682,9 @@ export interface ae_EventTrack extends ae_BaseObj {
*/
export interface ae_HostedFile extends ae_BaseObj {
hosted_file_id: string;
hosted_file_id_random: string;
hosted_file_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
hash_sha256?: string;
subdirectory_path?: string;
@@ -701,10 +701,10 @@ export interface ae_HostedFile extends ae_BaseObj {
*/
export interface ae_HostedFileLink {
id: string;
account_id_random: string;
hosted_file_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
hosted_file_id_random: string; // NO LONGER USE "_random"
link_to_type: string;
link_to_id_random: string;
link_to_id_random: string; // NO LONGER USE "_random"
created_on: string | Date;
updated_on: string | Date;
@@ -715,16 +715,16 @@ export interface ae_HostedFileLink {
*/
export interface ae_DataStore extends ae_BaseObj {
data_store_id: string;
data_store_id_random: string;
data_store_id_random: string; // NO LONGER USE "_random"
account_id?: null | string;
account_id_random?: null | string;
account_id_random?: null | string; // NO LONGER USE "_random"
for_type?: null | string;
for_id_random?: null | string;
for_id_random?: null | string; // NO LONGER USE "_random"
for_id?: null | string;
person_id_random?: null | string;
user_id_random?: null | string;
person_id_random?: null | string; // NO LONGER USE "_random"
user_id_random?: null | string; // NO LONGER USE "_random"
type?: null | string;
@@ -747,9 +747,9 @@ export interface ae_DataStore extends ae_BaseObj {
*/
export interface ae_Post extends ae_BaseObj {
post_id: string;
// post_id_random: string;
// post_id_random: string; // NO LONGER USE "_random"
account_id: string;
// account_id_random: string;
// account_id_random: string; // NO LONGER USE "_random"
title: string;
content: string;
@@ -769,9 +769,9 @@ export interface ae_Post extends ae_BaseObj {
*/
export interface ae_PostComment extends ae_BaseObj {
post_comment_id: string;
// post_comment_id_random: string;
// post_comment_id_random: string; // NO LONGER USE "_random"
post_id: string;
// post_id_random: string;
// post_id_random: string; // NO LONGER USE "_random"
content: string;
anonymous: boolean;
@@ -783,9 +783,9 @@ export interface ae_PostComment extends ae_BaseObj {
*/
export interface ae_Page extends ae_BaseObj {
page_id: string;
page_id_random: string;
page_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
alias: string;
title: string;
@@ -801,9 +801,9 @@ export interface ae_Page extends ae_BaseObj {
*/
export interface ae_Archive extends ae_BaseObj {
archive_id: string;
archive_id_random: string;
archive_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
archive_type?: string;
topic_id?: string;
@@ -824,8 +824,8 @@ export interface ae_Archive extends ae_BaseObj {
*/
export interface ae_ArchiveContent extends ae_BaseObj {
archive_content_id: string;
archive_content_id_random: string;
archive_id_random: string;
archive_content_id_random: string; // NO LONGER USE "_random"
archive_id_random: string; // NO LONGER USE "_random"
archive_content_type?: string;
content_html?: string;
@@ -833,7 +833,7 @@ export interface ae_ArchiveContent extends ae_BaseObj {
duration?: string;
hosted_file_id_random?: string;
hosted_file_id_random?: string; // NO LONGER USE "_random"
filename?: string;
subdirectory_path?: string;
}
@@ -843,15 +843,15 @@ export interface ae_ArchiveContent extends ae_BaseObj {
*/
export interface ae_EventFile extends ae_BaseObj {
event_file_id: string;
event_file_id_random?: string;
event_file_id_random?: string; // NO LONGER USE "_random"
event_id: string;
event_id_random?: string;
event_id_random?: string; // NO LONGER USE "_random"
hosted_file_id: string;
hosted_file_id_random?: string;
hosted_file_id_random?: string; // NO LONGER USE "_random"
for_type?: string | null;
for_id_random?: string | null;
for_id_random?: string | null; // NO LONGER USE "_random"
filename?: string;
filename_no_ext?: string;
@@ -868,10 +868,10 @@ export interface ae_EventFile extends ae_BaseObj {
*/
export interface ae_EventDevice extends ae_BaseObj {
event_device_id: string;
event_device_id_random: string;
event_device_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_location_id_random?: string;
event_id_random: string; // NO LONGER USE "_random"
event_location_id_random?: string; // NO LONGER USE "_random"
app_mode?: string;
status?: string;
@@ -886,9 +886,9 @@ export interface ae_EventDevice extends ae_BaseObj {
*/
export interface ae_EventAbstract extends ae_BaseObj {
event_abstract_id: string;
event_abstract_id_random: string;
event_abstract_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
external_id?: string;
abstract?: string;
@@ -901,9 +901,9 @@ export interface ae_EventAbstract extends ae_BaseObj {
*/
export interface ae_Organization extends ae_BaseObj {
organization_id: string;
organization_id_random: string;
organization_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
tagline?: string;
logo_path?: string;
@@ -915,14 +915,14 @@ export interface ae_Organization extends ae_BaseObj {
*/
export interface ae_EventRegistration extends ae_BaseObj {
event_registration_id: string;
event_registration_id_random: string;
event_registration_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
person_id: string;
person_id_random: string;
person_id_random: string; // NO LONGER USE "_random"
organization_id_random?: string;
contact_id_random?: string;
organization_id_random?: string; // NO LONGER USE "_random"
contact_id_random?: string; // NO LONGER USE "_random"
}
/**
@@ -930,13 +930,13 @@ export interface ae_EventRegistration extends ae_BaseObj {
*/
export interface ae_EventExhibit extends ae_BaseObj {
event_exhibit_id: string;
event_exhibit_id_random: string;
event_exhibit_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
organization_id_random?: string;
contact_id_random?: string;
person_id_random?: string;
organization_id_random?: string; // NO LONGER USE "_random"
contact_id_random?: string; // NO LONGER USE "_random"
person_id_random?: string; // NO LONGER USE "_random"
tagline?: string;
logo_path?: string;
@@ -948,13 +948,13 @@ export interface ae_EventExhibit extends ae_BaseObj {
*/
export interface ae_EventExhibitTracking extends ae_BaseObj {
event_exhibit_tracking_id: string;
event_exhibit_tracking_id_random: string;
event_exhibit_tracking_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_exhibit_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_exhibit_id_random: string; // NO LONGER USE "_random"
event_person_id_random?: string;
event_badge_id_random?: string;
event_person_id_random?: string; // NO LONGER USE "_random"
event_badge_id_random?: string; // NO LONGER USE "_random"
external_person_id?: string | null;
@@ -968,14 +968,14 @@ export interface ae_EventExhibitTracking extends ae_BaseObj {
*/
export interface ae_EventPerson extends ae_BaseObj {
event_person_id: string;
event_person_id_random: string;
event_person_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
person_id: string;
person_id_random: string;
person_id_random: string; // NO LONGER USE "_random"
event_badge_id_random?: string;
event_registration_id_random?: string;
event_badge_id_random?: string; // NO LONGER USE "_random"
event_registration_id_random?: string; // NO LONGER USE "_random"
passcode?: string;
agree_to_tc?: boolean;
@@ -986,10 +986,10 @@ export interface ae_EventPerson extends ae_BaseObj {
*/
export interface ae_EventPersonProfile extends ae_BaseObj {
event_person_profile_id: string;
event_person_profile_id_random: string;
event_person_profile_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_person_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_person_id_random: string; // NO LONGER USE "_random"
tagline?: string;
biography?: string;
@@ -1003,11 +1003,11 @@ export interface ae_EventPersonProfile extends ae_BaseObj {
* EventPersonTracking - Attendance and session interaction tracking
*/
export interface ae_EventPersonTracking extends ae_BaseObj {
event_person_id_random: string;
event_person_id_random: string; // NO LONGER USE "_random"
event_id: string;
event_id_random: string;
event_id_random: string; // NO LONGER USE "_random"
event_session_id_random?: string;
event_session_id_random?: string; // NO LONGER USE "_random"
check_in_out?: boolean;
in_datetime?: string | Date;
@@ -1019,11 +1019,11 @@ export interface ae_EventPersonTracking extends ae_BaseObj {
*/
export interface ae_Sponsorship extends ae_BaseObj {
sponsorship_id: string;
sponsorship_id_random: string;
sponsorship_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
sponsorship_cfg_id_random: string;
sponsorship_cfg_id_random: string; // NO LONGER USE "_random"
amount?: number;
paid?: boolean;
}
@@ -1033,9 +1033,9 @@ export interface ae_Sponsorship extends ae_BaseObj {
*/
export interface ae_SponsorshipCfg extends ae_BaseObj {
sponsorship_cfg_id: string;
sponsorship_cfg_id_random: string;
sponsorship_cfg_id_random: string; // NO LONGER USE "_random"
account_id: string;
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
level_li_json?: any;
start_datetime?: string | Date;
@@ -1046,7 +1046,7 @@ export interface ae_SponsorshipCfg extends ae_BaseObj {
* LogClientViewing - Playback and interaction tracking for media
*/
export interface ae_LogClientViewing extends ae_BaseObj {
account_id_random: string;
account_id_random: string; // NO LONGER USE "_random"
external_client_id: string;
object_type: string;

View File

@@ -1,6 +1,5 @@
<script lang="ts">
/** @type {import('./$types').LayoutData} */
// /** @type {import('./$types').LayoutProps} */
let log_lvl: number = 0;
@@ -11,13 +10,6 @@
import '../app.css';
// *** Import other supporting libraries
// import {
// ArrowBigRight,
// CircleX,
// RefreshCw,
// RefreshCcw,
// RefreshCcwDot
// } from '@lucide/svelte';
// Highlight JS
import hljs from 'highlight.js/lib/core';
@@ -39,13 +31,10 @@
// import Analytics from '$lib/app_components/e_app_analytics.svelte';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger, ae_auth_error } from '$lib/stores/ae_stores';
import { LoaderCircle } from '@lucide/svelte';
// import { events_loc, events_slct } from '$lib/stores/ae_events_stores';
// import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
import E_app_debug_menu from '$lib/app_components/e_app_debug_menu.svelte';
import E_app_sys_bar from '$lib/app_components/e_app_sys_bar.svelte';
import { pwa_install } from '$lib/pwa/pwa_install.svelte';
// import E_app_sys_menu from '$lib/app_components/e_app_sys_menu.svelte'; // replaced by E_app_sys_bar
interface Props {
data: any;

View File

@@ -211,7 +211,7 @@
text-gray-800 dark:text-gray-200
"
>
{#if !$ae_sess?.disable_sys_nav}
{#if !$ae_sess?.disable_sys_nav && !$ae_loc?.iframe}
<nav
bind:clientHeight={nav_y_height}
class:hidden={yTop > 600}
@@ -263,8 +263,8 @@
<section
class:iframe={$ae_loc?.iframe}
class:pt-0={!!$ae_sess?.disable_sys_nav}
class:pt-12={!$ae_sess?.disable_sys_nav && nav_y_height <= 50}
class:pt-0={!!$ae_sess?.disable_sys_nav || $ae_loc?.iframe}
class:pt-12={!$ae_sess?.disable_sys_nav && nav_y_height > 0 && nav_y_height <= 50}
class:pt-20={!$ae_sess?.disable_sys_nav && nav_y_height > 50 && nav_y_height <= 100}
class:pt-32={!$ae_sess?.disable_sys_nav && nav_y_height > 100 && nav_y_height <= 150}
class:pt-40={!$ae_sess?.disable_sys_nav && nav_y_height > 150 && nav_y_height <= 200}

View File

@@ -378,7 +378,7 @@
<!-- *** badge_front section start *** -->
<section
class="badge_front badge_type__{effective_badge_type_code.toLowerCase()}
flex flex-col gap-1
flex flex-col gap-0
items-stretch justify-between
min-h-[6.0in]
max-h-[6.0in]
@@ -394,12 +394,12 @@
>
<span
class="
print:hidden absolute top-1 left-4
text-xs italic
text-gray-500 group-hover:text-red-800
transition-all
"
>
print:hidden absolute top-1 left-4
text-xs italic
text-gray-500 group-hover:text-red-800
transition-all
"
>
{#if show_badge_back}Front of badge{:else}Badge preview{/if}
</span>
@@ -443,7 +443,8 @@
<div
class="badge_body
grow
max-h-[4in] m-0 p-0
m-0 p-0
px-1
overflow-clip
flex flex-col gap-1
items-stretch justify-between
@@ -578,7 +579,7 @@
{:then result}
{#if result}
<img
class="qr_code mecard_qr w-[0.75in] max-w-[0.75in]"
class="qr_code w-[1.10in] max-w-[1.10in] m-0 p-0"
src={result}
alt="Badge QR code"
/>
@@ -597,6 +598,7 @@
max-w-full
m-0 p-0
overflow-hidden
border-t-2 border-blue-500/75
flex flex-row gap-1 items-center justify-center
hover:outline-2 hover:outline-dashed hover:outline-gray-500/75
"

View File

@@ -154,7 +154,7 @@
const local_ids = local_results
.map((e) => String(e.id || e.event_exhibit_id))
.filter(Boolean);
if (current_search_id === last_search_id) {
if (log_lvl) console.log(`✅ [Trace] Exhibit Search #${current_search_id}: Local path found ${local_ids.length} items.`);
untrack(() => {
@@ -202,9 +202,9 @@
const api_ids = results
.map((e: any) => String(e.id || e.event_exhibit_id))
.filter(Boolean);
if (log_lvl) console.log(`📦 [Trace] Exhibit Search #${current_search_id}: API revalidation found ${api_ids.length} items.`);
untrack(() => {
exhibit_id_li = api_ids;
$events_sess.leads.submit_status__search = 'done';
@@ -241,9 +241,10 @@
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full max-w-6xl"
>
{#each $lq__event_exhibit_obj_li as exhibit_obj (exhibit_obj.event_exhibit_id)}
<!-- Force iframe mode (hides header/footer and passes exhibit_id via URL param) so the exhibit view can optimize for lead capture and hide irrelevant info. -->
<a
href="/events/{page.params
.event_id}/leads/exhibit/{exhibit_obj.event_exhibit_id}"
.event_id}/leads/exhibit/{exhibit_obj.event_exhibit_id}?iframe=true"
class="card card-hover p-4 flex flex-col items-center justify-center text-center space-y-2 preset-tonal"
>
<Store size="2em" />

View File

@@ -79,6 +79,8 @@
const show_hidden = search_params.show_hidden;
return raw_lead_li.filter(lead => {
// Never show disabled (removed) leads — enable=0/false means the exhibitor deleted them
if (lead.enable === 0 || lead.enable === false) return false;
// Exclude hidden leads unless show_hidden is toggled on
if (!show_hidden && lead.hide) return false;
if (licensee_filter === 'all') return true;
@@ -211,6 +213,8 @@
.where('event_exhibit_id')
.equals(target_exhibit_id)
.filter((tracking) => {
// 0. Never include disabled (removed) records — they're soft-deleted
if (!tracking.enable) return false;
// 1. Hide filter — exclude hidden records unless show_hidden is on
if (!params.show_hidden && tracking.hide) return false;

View File

@@ -27,7 +27,7 @@
.where('event_exhibit_id')
.equals(exhibit_id)
.toArray();
// Map badge_id -> tracking_id
const map = new Map();
leads.forEach(l => {
@@ -68,6 +68,7 @@
async function add_as_lead(badge: ae_EventBadge) {
// Use id or id_random — whichever is populated from search results
// NO LONGER USE "_random"
const badge_id = badge.event_badge_id_random || badge.event_badge_id;
if (!badge_id) {
console.warn('[add_as_lead] badge missing event_badge_id_random and event_badge_id', badge);
@@ -117,23 +118,23 @@
</script>
<div class="lead-manual-search space-y-4 w-full">
<form
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"
<form
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"
onsubmit={(e) => { e.preventDefault(); handle_search(); }}
>
<div class="flex flex-col md:flex-row items-center justify-center gap-1 grow">
<input
type="search"
<input
type="search"
bind:value={search_query}
placeholder="Attendee name, email, or badge ID..."
placeholder="Attendee name, email, or badge ID..."
class="input text-lg font-mono grow transition-all w-full"
/>
</div>
<div class="flex flex-row items-center justify-center gap-1">
<button
type="submit"
class="btn btn-lg preset-tonal-primary border border-primary-500 hover:preset-tonal-primary text-2xl font-bold w-48 transition-all"
<button
type="submit"
class="btn btn-lg preset-tonal-primary border border-primary-500 hover:preset-tonal-primary text-2xl font-bold w-48 transition-all"
disabled={searching}
>
{#if searching}

View File

@@ -11,12 +11,13 @@
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_api } from '$lib/stores/ae_stores';
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/ae_events_functions';
import Element_qr_scanner_v3 from '$lib/elements/element_qr_scanner_v3.svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { Camera, CircleAlert, CircleCheck, Eye, LoaderCircle, RefreshCw, ShieldOff, UserPlus, X } from '@lucide/svelte';
import { Camera, CircleAlert, CircleCheck, Eye, LoaderCircle, RefreshCw, RotateCcw, ShieldOff, UserPlus, X } from '@lucide/svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { ae_EventBadge } from '$lib/types/ae_types';
interface Props {
@@ -27,7 +28,8 @@
let { exhibit_id, scan_qualify = 'rapid', on_lead_added }: Props = $props();
// Track existing leads to prevent duplicates
// Track existing leads to detect duplicates and previously-removed records.
// Value includes tracking_id AND enabled status so we can offer re-activation.
let existing_leads_map = $derived(
liveQuery(async () => {
const leads = await db_events.exhibit_tracking
@@ -35,17 +37,21 @@
.equals(exhibit_id)
.toArray();
const map = new Map();
const map = new SvelteMap<string, { tracking_id: string; enabled: boolean }>();
leads.forEach(l => {
const b_id = l.event_badge_id_random || l.event_badge_id?.toString();
if (b_id) map.set(b_id, l.event_exhibit_tracking_id_random || l.event_exhibit_tracking_id?.toString());
const b_id = l.event_badge_id?.toString();
if (b_id) map.set(b_id, {
tracking_id: l.event_exhibit_tracking_id?.toString() ?? '',
// enable stored as 1/0 or true/false — !! normalises all falsy values
enabled: !!l.enable
});
});
return map;
})
);
let start_qr_scanner = $state(true);
let scanning_status = $state('idle'); // idle, scanning, found, adding, success, error, already_added, tracking_blocked
let scanning_status = $state('idle'); // idle, scanning, found, adding, success, error, already_added, tracking_blocked, reenable
let found_badge: ae_EventBadge | null = $state(null);
let existing_tracking_id = $state('');
let new_tracking_id = $state(''); // ID of the lead just created — used for "View Details" link
@@ -58,10 +64,12 @@
if (obj && obj.type === 'event_badge' && obj.id) {
start_qr_scanner = false;
// Check if already exists
// Check if already exists (enabled or disabled/removed)
if ($existing_leads_map?.has(obj.id)) {
scanning_status = 'already_added';
existing_tracking_id = $existing_leads_map.get(obj.id);
const existing = $existing_leads_map.get(obj.id)!;
existing_tracking_id = existing.tracking_id;
// Distinguish: active lead vs previously-removed lead
scanning_status = existing.enabled ? 'already_added' : 'reenable';
} else {
scanning_status = 'found';
}
@@ -95,7 +103,10 @@
}
async function confirm_add_lead() {
if (!found_badge || !found_badge.event_badge_id_random) return;
if (!found_badge || !found_badge.event_badge_id) {
console.warn('[leads] Guard failed — event_badge_id missing. found_badge:', found_badge);
return;
}
scanning_status = 'adding';
@@ -106,14 +117,14 @@
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,
event_badge_id: found_badge.event_badge_id,
external_person_id: user_email,
group: user_email
});
if (result) {
// Capture the new tracking ID so we can link to it
new_tracking_id = result.event_exhibit_tracking_id_random || String(result.event_exhibit_tracking_id || '');
new_tracking_id = String(result.event_exhibit_tracking_id || '');
scanning_status = 'success';
if (on_lead_added) on_lead_added(found_badge);
@@ -125,17 +136,68 @@
setTimeout(reset_scanner, 2000);
}
} else {
// API returned false/null — network error, API down, auth failure, or duplicate.
// Surface the error rather than leaving the scanner frozen at 'adding'.
scanning_status = 'error';
error_msg = 'Failed to add lead. Check your connection and try again.';
// API returned false/null — could be a duplicate of a previously-removed record
// that isn't in the local IDB cache (e.g. after IDB clear or on a second device).
// Search the API for a disabled tracking record for this badge before surfacing an error.
try {
const disabled_li = await events_func.search__exhibit_tracking({
api_cfg: $ae_api,
event_id: page.params.event_id,
event_exhibit_id: exhibit_id,
qry_badge_id: found_badge.event_badge_id,
enabled: 'not_enabled',
log_lvl: 1
});
if (disabled_li.length > 0) {
// Found a disabled record — offer to re-activate instead of showing an error
existing_tracking_id = String(disabled_li[0].event_exhibit_tracking_id || '');
scanning_status = 'reenable';
} else {
scanning_status = 'error';
error_msg = 'Failed to add lead. Check your connection and try again.';
}
} catch {
scanning_status = 'error';
error_msg = 'Failed to add lead. Check your connection and try again.';
}
}
} catch (e) {
} catch {
scanning_status = 'error';
error_msg = 'Failed to add lead. They might already be added.';
}
}
async function confirm_reenable_lead() {
// Re-activate a lead that was previously removed (enable=false).
// existing_tracking_id is already set from the map or the API fallback search.
if (!existing_tracking_id) return;
scanning_status = 'adding';
try {
const result = await events_func.update_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_id,
exhibit_tracking_id: existing_tracking_id,
data: { enable: true }
});
if (result) {
new_tracking_id = existing_tracking_id;
scanning_status = 'success';
if (on_lead_added && found_badge) on_lead_added(found_badge);
// Re-enabled lead: success card shows "View Details" link — user navigates manually.
// Auto-reset after 2s so the scanner is ready for the next badge.
setTimeout(reset_scanner, 2000);
} else {
scanning_status = 'error';
error_msg = 'Failed to restore lead. Please try again.';
}
} catch {
scanning_status = 'error';
error_msg = 'Failed to restore lead. Please try again.';
}
}
function reset_scanner() {
scanning_status = 'idle';
found_badge = null;
@@ -176,6 +238,43 @@
</button>
</div>
{:else if scanning_status === 'reenable'}
<div class="card p-6 w-full max-w-md space-y-4 preset-tonal-warning shadow-xl border-2 border-warning-500 animate-in zoom-in">
<div class="text-center space-y-2">
<RotateCcw size="3em" class="mx-auto text-warning-500" />
<h3 class="h3 font-bold">Previously Removed</h3>
<p class="text-xl font-bold">{found_badge?.full_name || 'Attendee'}</p>
<p class="opacity-70 text-sm">This lead was removed. Re-activate to restore their record including any saved notes and responses.</p>
</div>
<button
type="button"
class="w-full rounded-xl py-5 font-bold text-base flex items-center justify-center gap-2 bg-warning-500 text-white shadow-md hover:brightness-110 active:brightness-90 transition-all cursor-pointer"
onclick={confirm_reenable_lead}
>
<RotateCcw size="1.5em" />
Re-activate Lead
</button>
<a
href={`/events/${page.params.event_id}/leads/exhibit/${exhibit_id}/lead/${existing_tracking_id}`}
class="btn btn-sm w-full preset-outlined-warning"
class:hidden={!$ae_loc.trusted_access}
>
<Eye size="1em" />
View Existing Record
</a>
<button
type="button"
class="w-full rounded-lg py-3 text-sm font-medium flex items-center justify-center gap-2 border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-70"
onclick={reset_scanner}
>
<X size="1em" />
Cancel / Scan Again
</button>
</div>
{:else if scanning_status === 'already_added'}
<div class="card p-6 w-full max-w-md space-y-4 preset-tonal-secondary shadow-xl border-2 border-secondary-500 animate-in zoom-in">
<div class="text-center space-y-2">

View File

@@ -21,9 +21,10 @@
import { events_func } from '$lib/ae_events/ae_events_functions';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { ae_EventBadge } from '$lib/types/ae_types';
import { SvelteMap } from 'svelte/reactivity';
import {
CircleCheck, Eye, Layers, LoaderCircle,
RefreshCw, ScanLine, ShieldOff, UserPlus, X
RefreshCw, RotateCcw, ScanLine, ShieldOff, UserPlus, X
} from '@lucide/svelte';
interface Props {
@@ -38,7 +39,7 @@
const is_supported = typeof window !== 'undefined' && 'BarcodeDetector' in window;
// --- Types ---
type BatchStatus = 'loading' | 'ready' | 'blocked' | 'already_added' | 'adding' | 'added' | 'error';
type BatchStatus = 'loading' | 'ready' | 'blocked' | 'already_added' | 'reenable' | 'adding' | 'added' | 'error';
interface BatchItem {
id: string; // badge id_random from QR
@@ -49,14 +50,18 @@
}
// --- Existing leads (duplicate detection) ---
// Value includes tracking_id AND enabled status so we can offer re-activation for removed leads.
let existing_leads_map = $derived(
liveQuery(async () => {
const leads = await db_events.exhibit_tracking
.where('event_exhibit_id').equals(exhibit_id).toArray();
const map = new Map<string, string>();
const map = new SvelteMap<string, { tracking_id: string; enabled: boolean }>();
leads.forEach(l => {
const b_id = l.event_badge_id_random || l.event_badge_id?.toString();
if (b_id) map.set(b_id, l.event_exhibit_tracking_id_random || l.event_exhibit_tracking_id?.toString() || '');
const b_id = l.event_badge_id?.toString();
if (b_id) map.set(b_id, {
tracking_id: l.event_exhibit_tracking_id?.toString() || '',
enabled: !!l.enable
});
});
return map;
})
@@ -158,8 +163,10 @@
item.badge = badge;
if ($existing_leads_map?.has(item.id)) {
item.status = 'already_added';
item.existing_tracking_id = $existing_leads_map.get(item.id) ?? '';
const existing = $existing_leads_map.get(item.id)!;
item.existing_tracking_id = existing.tracking_id;
// Distinguish: active lead vs previously-removed lead
item.status = existing.enabled ? 'already_added' : 'reenable';
} else if (badge?.allow_tracking !== true) {
// Attendee has opted out — show card so staff can inform them
item.status = 'blocked';
@@ -172,7 +179,7 @@
}
async function add_lead(item: BatchItem) {
if (item.status !== 'ready' || !item.badge?.event_badge_id_random) return;
if (item.status !== 'ready' || !item.badge?.event_badge_id) return;
item.status = 'adding';
const user_email = $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.key || 'shared_passcode';
@@ -180,7 +187,7 @@
const result = await events_func.create_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_id,
event_badge_id: item.badge.event_badge_id_random,
event_badge_id: item.badge.event_badge_id,
external_person_id: user_email,
group: user_email
});
@@ -198,6 +205,29 @@
}
}
async function reenable_lead(item: BatchItem) {
if (!item.existing_tracking_id) return;
item.status = 'adding';
try {
const result = await events_func.update_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_id,
exhibit_tracking_id: item.existing_tracking_id,
data: { enable: true }
});
if (result) {
item.status = 'added';
if (on_lead_added && item.badge) on_lead_added(item.badge);
setTimeout(() => dismiss_item(item), 1000);
} else {
item.status = 'error';
}
} catch {
item.status = 'error';
}
}
async function add_all() {
const to_add = batch.filter(i => i.status === 'ready' && !i.dismissing);
await Promise.all(to_add.map(add_lead));
@@ -359,6 +389,33 @@
</button>
</div>
{:else if item.status === 'reenable'}
<div class="flex items-start gap-2">
<RotateCcw size="1.2em" class="text-warning-500 shrink-0 mt-0.5" />
<div>
<p class="font-bold text-sm leading-tight">{item.badge?.full_name || 'Attendee'}</p>
<p class="text-xs opacity-60 mt-0.5">Previously removed</p>
</div>
</div>
<div class="flex gap-2">
<button
type="button"
class="flex-1 rounded-lg py-2 text-xs font-bold flex items-center justify-center gap-1.5 bg-warning-500 text-white hover:brightness-110 transition-all cursor-pointer"
onclick={() => reenable_lead(item)}
>
<RotateCcw size="0.9em" />
Re-activate
</button>
<button
type="button"
class="flex-none rounded-lg px-3 py-2 flex items-center justify-center border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-60"
title="Skip"
onclick={() => dismiss_item(item)}
>
<X size="1em" />
</button>
</div>
{:else if item.status === 'ready'}
<div>
<p class="font-bold text-sm leading-tight">{item.badge?.full_name || 'Badge Found'}</p>

View File

@@ -4,13 +4,15 @@
* Lead Detail View - Basic Read-Only version.
*/
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import Element_ae_obj_field_editor_v3 from '$lib/elements/element_ae_obj_field_editor_v3.svelte';
import Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte';
import { Briefcase, CalendarDays, ChevronLeft, Eye, FileText, ListTodo, LoaderCircle, Mail, MapPin, ShieldCheck, SquarePen, Star, Store, User } from '@lucide/svelte';
import { Briefcase, CalendarDays, ChevronLeft, Eye, FileText, ListTodo, LoaderCircle, Mail, MapPin, RotateCcw, ShieldCheck, SquarePen, Star, Store, Trash2, User } from '@lucide/svelte';
const exhibit_tracking_id = $derived(page.params.exhibit_tracking_id);
let lq__lead_obj = $derived(
@@ -30,6 +32,46 @@
let is_edit_mode = $state(false);
// Remove / Restore flow.
// Two-click confirm for remove: idle → confirm → removing → (navigate back).
let remove_status = $state<'idle' | 'confirm' | 'removing' | 'restoring'>('idle');
async function remove_lead() {
const eid = page.params.exhibit_id ?? '';
if (!exhibit_tracking_id || !eid) return;
remove_status = 'removing';
try {
await events_func.update_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_id: eid,
exhibit_tracking_id,
data: { enable: false }
});
// Navigate back to exhibit leads list after removal
goto(`/events/${page.params.event_id}/leads/exhibit/${eid}`);
} catch {
// If update fails, reset so user can try again
remove_status = 'idle';
}
}
async function restore_lead() {
const eid = page.params.exhibit_id ?? '';
if (!exhibit_tracking_id || !eid) return;
remove_status = 'restoring';
try {
await events_func.update_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_id: eid,
exhibit_tracking_id,
data: { enable: true }
});
remove_status = 'idle';
} catch {
remove_status = 'idle';
}
}
// Helper to format date using Aether utility
function format_date(date: any) {
if (!date) return '';
@@ -57,11 +99,11 @@
<div class="flex items-center gap-2">
{#if $lq__lead_obj}
<button
<button
class="btn btn-sm"
class:preset-filled-primary={is_edit_mode}
class:preset-outlined-surface={!is_edit_mode}
onclick={() => is_edit_mode = !is_edit_mode}
onclick={() => { is_edit_mode = !is_edit_mode; remove_status = 'idle'; }}
>
{#if is_edit_mode}
<Eye size="1.2em" class="mr-1" /> View
@@ -69,6 +111,40 @@
<SquarePen size="1.2em" class="mr-1" /> Edit
{/if}
</button>
<!-- Remove Lead — two-click confirm to prevent accidental removal.
Removing only sets enable=false (soft-delete); the record can be restored. -->
{#if $lq__lead_obj.enable}
{#if remove_status === 'confirm'}
<button
type="button"
class="btn btn-sm preset-filled-error font-bold"
onclick={remove_lead}
>
<Trash2 size="1em" />
Confirm Remove?
</button>
<button
type="button"
class="btn btn-sm preset-outlined-surface opacity-60"
onclick={() => remove_status = 'idle'}
>Cancel</button>
{:else}
<button
type="button"
class="btn btn-sm preset-outlined-error opacity-70"
disabled={remove_status === 'removing'}
onclick={() => remove_status = 'confirm'}
>
{#if remove_status === 'removing'}
<LoaderCircle size="1em" class="animate-spin" />
{:else}
<Trash2 size="1em" />
{/if}
Remove
</button>
{/if}
{/if}
{/if}
{#if $lq__lead_obj?.priority}
@@ -238,28 +314,33 @@
</div>
</div>
<!-- Status Indicator -->
<div class="card p-4 flex items-center gap-3 preset-tonal-surface shadow-sm">
{#if is_edit_mode}
<div class="flex-1 flex items-center justify-between">
<div class="font-bold text-sm">Enabled</div>
<Element_ae_obj_field_editor_v3
object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id ?? ''}
field_name="enable"
field_type="checkbox"
current_value={$lq__lead_obj.enable}
object_reload={true}
/>
<!-- Restore Lead card — only shown when lead has been removed (enable=false/0).
Removing sets enable=false rather than deleting so notes/responses are preserved. -->
{#if !$lq__lead_obj.enable}
<div class="card p-4 space-y-3 preset-tonal-error border border-error-500/50 shadow-sm">
<div class="flex items-center gap-2">
<ShieldCheck size="1.4em" class="text-error-500 shrink-0" />
<div>
<div class="font-bold text-sm">Lead Removed</div>
<div class="text-[10px] opacity-60 uppercase font-black">Not visible in leads list</div>
</div>
</div>
{:else}
<ShieldCheck size="1.5em" class={$lq__lead_obj.enable ? 'text-success-500' : 'text-error-500'} />
<div>
<div class="font-bold">{$lq__lead_obj.enable ? 'Record Enabled' : 'Record Disabled'}</div>
<div class="text-[10px] opacity-50 uppercase font-black">Visibility Status</div>
</div>
{/if}
</div>
<button
type="button"
class="btn btn-sm w-full preset-filled-success font-bold"
disabled={remove_status === 'restoring'}
onclick={restore_lead}
>
{#if remove_status === 'restoring'}
<LoaderCircle size="1em" class="animate-spin" />
Restoring...
{:else}
<RotateCcw size="1em" />
Restore Lead
{/if}
</button>
</div>
{/if}
</div>
</div>

View File

@@ -22,11 +22,11 @@ const template_id = 'jgfixEpYp1B';
const mock_badge = {
id: badge_id,
event_badge_id: badge_id,
event_badge_id_random: badge_id,
event_badge_id_random: badge_id, // NO LONGER USE "_random"
event_id: event_id,
event_id_random: event_id,
event_badge_template_id: template_id,
event_badge_template_id_random: template_id,
event_badge_template_id_random: template_id, // NO LONGER USE "_random"
full_name_override: 'Scott Idem',
given_name: 'Scott',
family_name: 'Idem',
@@ -41,7 +41,7 @@ const mock_badge = {
const mock_template = {
id: template_id,
id_random: template_id,
id_random: template_id, // NO LONGER USE "_random"
event_badge_template_id: template_id,
badge_template_id: template_id,
event_id: event_id,

View File

@@ -42,11 +42,11 @@ function make_badge(badge_id: string, template_id: string, overrides: Record<str
return {
id: badge_id,
event_badge_id: badge_id,
event_badge_id_random: badge_id,
event_badge_id_random: badge_id,// NO LONGER USE "_random"
event_id: event_id,
event_id_random: event_id,
event_id_random: event_id,// NO LONGER USE "_random"
event_badge_template_id: template_id,
event_badge_template_id_random: template_id,
event_badge_template_id_random: template_id,// NO LONGER USE "_random"
full_name: 'Jane Doe',
full_name_override: null,
given_name: 'Jane',