Files
OSIT-AE-App-Svelte/documentation/MODULE__AE_Events_Badges.md

20 KiB

MODULE: Aether Events — Badges

Module Path: src/routes/events/[event_id]/(badges)/badges/ API Module: src/lib/ae_events/ae_events__event_badge.ts Database: db_events.badge (Dexie IndexedDB table) Last Updated: 2026-02-26


Overview

The Badges module manages event attendee badges with support for:

  • External system imports (iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
  • Field override protection to prevent staff/attendee edits from being overwritten by automated syncs
  • Multi-tier access control for field editing
  • QR code generation for badge scanning
  • Print tracking (count, first/last print datetime)
  • Advanced search and filtering

Critical Design Pattern: Override Fields

Purpose

The *_override fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because:

  1. Staff may need to correct imported data
  2. Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
  3. External systems often have outdated or incorrect data
  4. Changes should persist across multiple sync cycles

How It Works

Import Behavior:

External System → Aether API → Populates REGULAR fields only
                                (never touches *_override fields)

Display Behavior:

UI Display Logic:
1. IF `*_override` field has value → USE IT (highest priority)
2. ELSE IF regular field has value → USE IT (fallback)
3. ELSE → Display placeholder/empty

Example — Full Name:

// API imports from iMIS
badge.given_name = "Robert"
badge.family_name = "Smith"
badge.full_name = "Robert Smith"  // Auto-computed

// Staff edits to preferred name
badge.full_name_override = "Bob Smith"

// Display in UI
display_name = badge.full_name_override || badge.full_name || "-- no name --"
// Result: "Bob Smith"

// Next cron sync from iMIS
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
// ✅ badge.full_name_override remains "Bob Smith" (PROTECTED)
// ✅ Display still shows "Bob Smith"

Override Fields (7 total)

Regular Field Override Field Purpose Editable By
professional_title professional_title_override Job title display Staff, Attendee
full_name full_name_override Preferred name, pronouns Staff, Attendee
affiliations affiliations_override Organization display Staff, Attendee
email email_override Contact email override Staff only
location location_override City/State/Country display Staff, Attendee
badge_type badge_type_override Badge category label (varies per Event/Template) Staff only
badge_type_code badge_type_code_override Badge access level code (varies per Event/Template) Staff only

Sync Safety Rules

Automated Sync (Cron Jobs):

  • CAN update: All regular fields (given_name, family_name, email, affiliations, etc.)
  • CANNOT update: Any *_override field
  • CANNOT delete: Any *_override value

Manual Staff Edit:

  • CAN update: Any field (including overrides)
  • CAN clear: Override fields (reverts to regular field)

Attendee Self-Service Edit:

  • CAN update: Only specific override fields (per event config)
  • CAN clear: Their own override fields
  • CANNOT edit: Regular fields, badge_type, email_override

External System Integration

Supported Import Sources

  • iMIS (Association Management)
  • Zoom (Virtual event registration)
  • Novi AMS (Association Management)
  • Impexium (Association Management)
  • Confex (Event abstract management)
  • Cvent (Event registration)
  • Custom CSV/Excel imports

Data Flow Direction

External Systems ─────────> Aether
    (READ ONLY)          (WRITE + DISPLAY)

Important: Aether is pull-only — does not push changes back to external systems. This prevents sync conflicts and maintains external systems as the source of truth for base data.

Sync Behavior

  • Frequency: Scheduled cron jobs (typically hourly, daily, or on-demand)
  • Method: Full sync or incremental (depends on external system API)
  • Conflict Resolution: Override fields always win

Pseudocode:

def sync_badge_from_external(external_badge_data, existing_badge):
    # Update regular fields from external source
    existing_badge.given_name = external_badge_data.first_name
    existing_badge.family_name = external_badge_data.last_name
    existing_badge.email = external_badge_data.email
    existing_badge.affiliations = external_badge_data.organization
    existing_badge.badge_type_code = external_badge_data.registration_type

    # NEVER TOUCH OVERRIDE FIELDS
    # existing_badge.full_name_override ← PROTECTED
    # existing_badge.affiliations_override ← PROTECTED
    # existing_badge.email_override ← PROTECTED

    return existing_badge

Access Control & Edit Permissions

Access Levels (Ascending)

  1. Anonymous — No access to badges
  2. Public — View public event info only (no badge access)
  3. Authenticated — View own badge, limited self-edit
  4. Trusted — Search all badges, view all, edit own
  5. Administrator — Full CRUD, bulk operations, override any field
  6. Manager — All administrator + event configuration
  7. Super — All manager + cross-event operations

Current Implementation (v3)

Quick Edit Feature ([badge_id]/+page.svelte → ae_comp__badge_obj_view.svelte)

Edit Mode Trigger:

  • URL hash #review enables edit mode
  • Sets $ae_loc.edit_mode = true
  • Shows Save/Cancel buttons

Currently Editable Fields:

// Lines 90-96 in ae_comp__badge_obj_view.svelte
editable_full_name_override: string | null
editable_professional_title_override: string | null
editable_affiliations_override: string | null  // textarea
editable_location_override: string | null
editable_allow_tracking: boolean | null
editable_email: string | null
editable_badge_type_code: string | null

UI Components:

  • Input fields shown when edit_mode_active === true
  • Save button calls handle_save_changes() (Line 365)
  • Cancel button calls handle_cancel_changes() (Line 450)
  • Only changed fields sent to API (update_ae_obj__event_badge)

Future Planned Enhancement

Event-Level Configuration: event.mod_badges_json.edit_permissions

{
  "authenticated": {
    "can_edit": ["full_name_override", "professional_title_override", "affiliations_override", "location_override"],
    "requires_approval": false
  },
  "trusted": {
    "can_edit": ["full_name_override", "professional_title_override", "affiliations_override", "location_override", "email_override"],
    "can_view_all": true,
    "requires_approval": false
  },
  "administrator": {
    "can_edit": "*",
    "can_bulk_edit": true,
    "can_delete": true
  }
}

Status: Placeholder UI exists, JSON config not yet implemented.


Search & Filter Capabilities

Search Component

File: ae_comp__badge_search.svelte

Available Filters

Fulltext Search (All Users)

  • Searches: default_qry_str database field
  • Includes: Name, email, external IDs
  • Type: LIKE %query% (case-insensitive)
  • Trigger: Enter key or 3+ characters typed

Advanced Filters (Trusted Access & Above)

// Badge Type Filter
badge_type_code: 'current_member' | 'inactive_member' | 'ex_all' | 'staff' | etc.
// Note: Badge types are defined per Event and Event Badge Template in database table records.
// Common types include: member, nonmember, guest, exhibitor, staff
// This is a work in progress - types vary by event configuration.

// Print Status Filter
qry_printed_status: 'all' | 'printed' | 'not_printed'

// Affiliations Search
qry_affiliations: string  // Separate filter for organization search

// Sort Options
qry_sort_order:
  - 'name_asc' / 'name_desc'
  - 'updated_desc' / 'updated_asc'
  - 'print_count_desc'
  - 'print_first_desc' / 'print_last_desc'
  - 'badge_type_asc'
  - 'affiliations_asc'
  • Scans badge QR code
  • Extracts badge ID
  • Auto-fills search with ID
  • Jumps to badge detail view

Search Implementation Pattern

File: badges/+page.svelte (Lines 117-365)

Strategy: Standardized Reactive Search Pattern (Aether UI V3)

  1. Isolate dependencies into stable $derived object
  2. Debounced effect (300ms) triggers search
  3. Fast Path: Search IDB first (if not remote_first)
  4. Revalidate: API request updates IDB
  5. LiveQuery: UI auto-updates from IDB changes

Search API: events_func.search__event_badge()

await search__event_badge({
  api_cfg: $ae_api,
  event_id: event_id,
  fulltext_search_qry_str: qry_str || null,
  type_code: type_code || null,
  printed_status: printed_status,
  affiliations_qry_str: aff_str || null,
  order_by_li: order_by_li,
  limit: 150,
  log_lvl: 0
})

Badge Display Logic

Name Display Priority

// Component: ae_comp__badge_obj_li.svelte (Lines 113-121)
if (event_badge_obj?.full_name_override)
    display: full_name_override
else if (event_badge_obj?.full_name)
    display: full_name
else
    display: given_name + ' ' + family_name

Badge View Page

Route: /events/[event_id]/badges/[badge_id]

Components:

  • +page.svelte — Container with LiveQuery for badge data
  • ae_comp__badge_obj_view.svelte — Full badge display + edit UI

LiveQueries:

lq__event_badge_obj = liveQuery(() => db_events.badge.get(event_badge_id))
lq__event_badge_template_obj = liveQuery(() =>
  db_events.badge_template.get(badge.event_badge_template_id)
)

Loading States:

  • is_loading_idb — Waiting for initial IDB lookup
  • If badge not found → "Badge Not Found" error with reload button
  • Loader spinner while fetching

Badge Templates

Purpose

Badge templates define the visual layout and content structure for printed badges:

  • Header images/logos
  • Field positions and font sizes
  • QR code placement
  • Ticket/option indicator display
  • WiFi credentials display

Template Selection

Each badge references an event_badge_template_id. The template controls:

  • Layout (front/back)
  • Branding elements
  • Which fields to show
  • Field formatting rules

Template Loading

Templates are loaded alongside badges via inc_template parameter:

load_ae_obj_id__event_badge({
  event_badge_id: badge_id,
  inc_template: true  // Also loads template
})

Print Tracking

Print Fields

print_count: number             // Increments each print
print_first_datetime: string    // ISO datetime of first print
print_last_datetime: string     // ISO datetime of most recent print

Print Workflow (Future)

  1. Pre-Print: Check print_count to warn if already printed
  2. Print: Send to printer via Electron native bridge or browser print
  3. Post-Print: Increment print_count, update print_last_datetime
  4. Audit: Print history available for staff review

Current Status: UI placeholder for print button, tracking fields exist in database, print functionality pending.


Database Schema

IndexedDB Table: badge

File: src/lib/ae_events/db_events.ts (Lines 841-852)

Indexed Fields:

badge: `
  event_badge_id_random, event_badge_id, id,
  event_id, event_id_random,
  full_name, full_name_override, email, email_override,
  affiliations, affiliations_override,
  badge_type, badge_type_code, badge_type_code_override, badge_type_override,
  external_event_id, external_id, external_person_id,
  default_qry_str,
  alert,
  tmp_sort_1, tmp_sort_2,
  print_count, print_first_datetime, print_last_datetime,
  enable, hide, priority, sort, group, notes, created_on, updated_on
`

Saved Properties

File: ae_events__event_badge.ts (Lines 495-563)

Complete field list (67 fields total):

  • Identity: id, event_badge_id, event_id, event_badge_template_id
  • Name: pronouns, informal_name, title_names, given_name, middle_name, family_name, designations
  • Professional: professional_title, professional_title_override
  • Display: full_name, full_name_override
  • Organization: affiliations, affiliations_override
  • Contact: email, email_override
  • Address: address_line_1, address_line_2, address_line_3, city, country_subdivision_code, state_province, state_province_abb, postal_code, country_alpha_2_code, country, full_address
  • Location: location, location_override
  • Classification: badge_type, badge_type_code, badge_type_override, badge_type_code_override
  • External: external_event_id, external_id, external_person_id
  • Search: query_str, default_qry_str
  • System: alert, enable, hide, priority, sort, group, notes, created_on, updated_on
  • Print: print_count, print_first_datetime, print_last_datetime
  • Sorting: tmp_sort_1, tmp_sort_2
  • Person Link: person_external_id, person_external_sys_id, person_given_name, person_family_name, person_full_name, person_professional_title, person_affiliations, person_primary_email, person_passcode

API Functions

CRUD Operations

File: src/lib/ae_events/ae_events__event_badge.ts

// Load single badge
load_ae_obj_id__event_badge({ event_badge_id, event_id, inc_template })

// Load badge list
load_ae_obj_li__event_badge({ event_id, view, limit, order_by_li })

// Search badges (V3 API)
search__event_badge({
  event_id,
  fulltext_search_qry_str,
  type_code,
  printed_status,
  affiliations_qry_str,
  order_by_li
})

// Create badge
create_ae_obj__event_badge({ event_id, data_kv })

// Update badge
update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })

// Delete badge
delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })

Field Processing

Function: process_ae_obj__event_badge_props()

Processing Steps:

  1. Map *_random fields to clean names (event_badge_id_randomevent_badge_id)
  2. Set primary id field from event_badge_id
  3. Ensure event_id is set (from function parameter if missing)
  4. Calculate tmp_sort_1 and tmp_sort_2 for efficient sorting
  5. Return processed objects

Critical Fix (2026-02-26): All CRUD functions now return processed data (matches IDB cache) instead of raw API responses. This ensures consistency between function return values and cached data.


Component Architecture

Route Structure

/events/[event_id]/(badges)/badges/
├── +layout.svelte          # Layout wrapper (minimal)
├── +page.svelte            # Badge list + search
├── ae_comp__badge_search.svelte    # Search form + filters
├── ae_comp__badge_obj_li.svelte    # Badge list display
├── ae_comp__badge_create_form.svelte   # (Not actively used)
├── ae_comp__badge_upload_form.svelte   # Bulk CSV upload
└── [badge_id]/
    ├── +page.svelte        # Badge detail container
    ├── +page.ts            # Badge loader (non-blocking)
    └── ae_comp__badge_obj_view.svelte  # Badge display + edit

Key Components

Badge List Page (+page.svelte)

  • LiveQuery: Reactive badge list from IDB
  • Search Pattern: Debounced search with fast path + revalidation
  • ID List: event_badge_id_li drives LiveQuery
  • Loading State: Shows spinner when search_status === 'loading'

Badge Search (ae_comp__badge_search.svelte)

  • Form Mode: Toggle between search form and QR scanner
  • Filters: Badge type, print status, affiliations, sort order (trusted+ only)
  • Fulltext: Name/email search (all users)
  • QR Scan: Integrated QR scanner for badge ID lookup

Badge List Display (ae_comp__badge_obj_li.svelte)

  • Visibility Filter: Respects hide flag (trusted+ sees all)
  • Display Logic: Override → regular → fallback pattern
  • Print Indicator: Green checkmark badge shows print_count
  • Metadata: ID, created/updated timestamps (edit mode only)

Badge Detail View (ae_comp__badge_obj_view.svelte)

  • Edit Mode: Activated by #review URL hash or edit button
  • Form Binding: Direct bind:value on editable fields
  • Dynamic Sizing: Font size adjusts based on text length
  • Print Preview: Full badge layout with template
  • Save Handler: Only sends changed fields to API

Testing Status

Current Test Coverage

  • Badge list cold-start (failing — mock API issue)
  • Badge data integrity (failing — mock API issue)
  • Badge template list (failing — route mismatch)
  • Badge type filter tests
  • Badge template relationship tests
  • Electron bridge compatibility (graceful degradation)

Test Issues

Root Cause: Mock API routes not matching actual request patterns. Tests provide correct mock data but page shows "No badges found".

Browser Error: Object is missing a valid ID for table "badge" — Mock data reaching IDB with only {tmp_sort_1, tmp_sort_2, event_id}, all other fields stripped.

Fix Required: Debug actual API request URLs, adjust mock route patterns.

Manual Testing Recommended: Navigate to /events/{event_id}/badges in browser to verify actual functionality before fixing test infrastructure.


Known Issues & Future Enhancements

Known Issues

  1. Test Infrastructure: Mock API routes not connecting to page requests
  2. Session Cold-Start: Potential race condition on first load (same as pres mgmt module)
  3. Type Definitions: Some TypeScript errors on external package types (pre-existing)

Future Enhancements

  1. Access-Based Edit Permissions: Implement JSON config for field-level access control
  2. Print Functionality: Wire up print button to Electron native print or browser print API
  3. Batch Operations: Bulk update, bulk print, bulk export
  4. Audit Log: Track who edited which fields and when
  5. Photo Badges: Support badge photo upload and display
  6. Custom Badge Layouts: Dynamic template selection per badge type
  7. Real-Time Sync: WebSocket updates for multi-device badge printing stations
  8. Approval Workflow: Require manager approval for certain field changes

Development Guidelines

Adding New Override Fields

  1. Add {field}_override to database schema
  2. Add to properties_to_save array in ae_events__event_badge.ts
  3. Update display logic to check override first
  4. Add to editable fields in ae_comp__badge_obj_view.svelte
  5. Update access control config
  6. Document in this file

Testing Override Fields

// Simulate external sync
badge.given_name = "External Value"

// User edits
badge.given_name_override = "User Value"

// Next sync (should NOT change override)
badge.given_name = "Updated External Value"

// Display should still show "User Value"
assert(display === badge.given_name_override)

Debugging Search Issues

// Enable search logging
log_lvl: 2

// Check search params object
console.log('Search params:', search_params)

// Verify API request
console.log('API request:', { event_id, fulltext_search_qry_str, type_code })

// Check returned IDs
console.log('Badge IDs:', event_badge_id_li)

// Verify IDB contents
db_events.badge.toArray().then(console.log)


Document Status: Complete Last Verified: 2026-02-26 Verified Against: Code commit b28595da (badge data fixes)