Files
OSIT-AE-App-Svelte/documentation/MODULE__AE_Events_Badges.md
2026-03-24 11:15:01 -04:00

36 KiB
Raw Blame History

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-27 (rev 6) Related Docs: documentation/PROJECT__AE_Events_Badges_Review_Print.md (implementation guide)


Overview

The Badges module manages event attendee badges with support for:

  • External system imports as needed (CSV/Excel, 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
  • HTML rendering in display fields for rich text formatting
  • Accessibility features (text enlargement toggle)

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

HTML Rendering (implemented 2026-02-27):

Certain fields support HTML markup for rich text formatting. When viewing (not editing), these fields use Svelte's {@html} directive to render the markup:

  • full_name / full_name_override
  • professional_title / professional_title_override
  • affiliations / affiliations_override
  • location / location_override

This allows for formatting like:

  • Bold/italic: <b>Dr.</b> Jane Smith or <i>Chief Medical Officer</i>
  • Line breaks: Hospital Name<br>Department Name
  • Special characters and entities

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 with HTML
badge.full_name_override = "<b>Bob</b> Smith"

// Display in UI (review form)
{@html badge.full_name_override || badge.full_name || "— no name —"}
// Result: **Bob** Smith (bold rendered)

// Edit mode shows raw HTML
<input bind:value={editable_full_name_override} />
// Shows: <b>Bob</b> Smith (editable as text)

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

Override Fields

Regular Field Override Field Purpose Editable By HTML Rendering
pronouns pronouns_override Preferred pronouns Staff, Attendee No
professional_title professional_title_override Job title display Staff, Attendee Yes
full_name full_name_override Preferred name display Staff, Attendee Yes
affiliations affiliations_override Organization display Staff, Attendee Yes
phone phone_override Phone number Staff, Attendee No
email email_override Contact email override Staff only No
location location_override City/State/Country display Staff, Attendee Yes
badge_type badge_type_override Badge category label text Staff only No
badge_type_code badge_type_code_override Badge access level code Staff only No
registration_type registration_type_override Registration category label text Staff only No
registration_type_code registration_type_code_override Registration category code Staff only No

Note: phone, phone_override, pronouns_override, registration_type, registration_type_code, registration_type_override, registration_type_code_override may need to be confirmed against the DB schema via ae_describe event_badge and added to properties_to_save in ae_events__event_badge.ts if not already present.

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) — 2026-02-27

Badge Search Results Visibility

Access Level Sees
Below Trusted (incl. anonymous) Only badges where print_count < 1 and not hidden
Trusted, not Edit Mode Only badges where print_count < 1 and not hidden
Trusted + Edit Mode All badges where hide === false (including already-printed)

Print Button Behavior (per result row)

Access Level Print Action
Below Trusted No print action — name shown with User icon, non-interactive
Trusted, print_count < 1 Clickable link → /print page, Printer icon
Trusted, print_count >= 1, not Edit Mode Disabled (already printed safety lock), shows Nx count
Trusted, print_count >= 1, Edit Mode Clickable reprint — shows Nx count badge next to icon

Print count displayed as [Printer][2×] Name when print_count >= 1.

Review Area Buttons (per result row, up to 3 buttons total)

Button Visible To Behavior
Email Review Link All users Placeholder alert() — will trigger email API
Review Link (clipboard) Trusted + Edit Mode only Copies /review URL to clipboard; shows Copied! feedback
(direct Review link) (future) (not yet implemented as separate nav button)

Badge Edit Form (ae_comp__badge_obj_view.svelte)

Currently editable fields (local edit_mode_active, not global edit_mode):

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
  • Save button → handle_save_changes() — only changed fields sent to API
  • Cancel button → handle_cancel_changes() — reverts to IDB values
  • IMPORTANT: This component must NEVER write to $ae_loc.edit_mode — it uses its own local edit_mode_active flag only. (Bug fixed 2026-02-27)

Badge Review Form (ae_comp__badge_review_form.svelte)

Form-based review (NOT a badge render). Used by the /review page.

Props:

  • can_edit_fields: string[] prop controls which fields are editable per user level
  • ['*'] = administrator (all fields)
  • is_staff: boolean prop shows/hides the staff-only fields
  • Fields show "(overridden)" label when an override value differs from the base field

Features (implemented 2026-02-27):

  • HTML Rendering: full_name_override, professional_title_override, affiliations_override, and location_override fields render HTML markup using {@html} directive when viewing (not when editing)
  • Accessibility: Text enlargement toggle button switches between text-2xl (normal) and text-4xl (enlarged) for improved readability
  • Help Modal: Flowbite Modal component with 6 help sections (Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance)
  • QR Code Display: Generates QR code using core_func.js_generate_qr_code() with badge ID, supports hover zoom and click-to-expand
  • Print Status: Shows print count, first print datetime, and last print datetime at top of form
  • Local Edit Mode: Independent local_edit_active state (never writes to $ae_loc.edit_mode)
  • Save/Cancel: Only changed fields sent to API; revert button for override fields

Editable Fields:

  • Pronouns, Full Name, Professional Title, Affiliations, Phone, Location (all with override support)
  • Allow Tracking checkbox (lead scanning permission)
  • Staff-only: Email, Badge Type, Registration Type, Hide, Priority, Notes
  • Staff-only: Options (other_1_code through other_8_code) and Tickets (ticket_1_code through ticket_8_code)
  • Agree to Terms & Conditions checkbox (attendee-visible when in can_edit_fields)

Badge Review Page — Header Buttons (implemented 2026-02-27)

Button Visible To Behavior
Back → Search (ArrowLeft) Staff (has_staff_access) only <a href="/events/{id}/badges">
Print (Printer icon) Trusted+, not printed OR Trusted+Edit if printed <a href="/print">, shows Nx count if reprinting
Copy Link (clipboard) Trusted + Edit Mode only Copies review URL to clipboard; Copied! feedback for 2s
Email Link (Mail icon) All if not printed; Trusted+Edit if printed Placeholder alert() — email API pending

Badge Print Page — Header Buttons (implemented 2026-02-27)

Button Visible To Behavior
Back → Search (ArrowLeft) Always (when badge loaded) <a href="/events/{id}/badges">
Print Now (Printer icon) Trusted+, not printed OR Trusted+Edit if printed Calls window.print() directly (convenience duplicate); print count tracked by component button
Review (Eye icon) Trusted + Edit Mode only <a href="/review"> nav link
Email Link (Mail icon) All if not printed; Trusted+Edit if printed Placeholder alert() — email API pending

Badge Review Page — Display Sections (implemented 2026-02-27)

The review form (ae_comp__badge_review_form.svelte) displays:

  1. Print status — print count + first/last print timestamps (read-only, hidden if never printed)
  2. QR Code — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). Generated using core_func.js_generate_qr_code() with obj_type: 'event_badge' and badge ID. Supports hover zoom overlay and click-to-expand.
  3. Editable Fields — all fields with access-level gating, override support, and HTML rendering for display fields
  4. Options (other_1_code through other_8_code) — Staff: editable text inputs; Attendees: shown as [✓] Option X checkmark display only when value exists
  5. Tickets (ticket_1_code through ticket_8_code) — Staff: editable text inputs; Attendees: shown as [✓] Ticket X checkmark display only when value exists
  6. Accessibility Toggle — Font size enlargement button in sticky header (text-2xl ↔ text-4xl)
  7. Help Modal — Attendee guidance modal with 6 sections explaining the review process, editing, QR codes, and lead scanning

Default Field Permissions (hardcoded for now — Axonius first show, mid-April 2026)

These are hardcoded in review/+page.svelte pending connection to mod_badges_json.edit_permissions.

Attendee (passcode-authenticated / anonymous with link):

[
  'pronouns_override',
  'full_name_override',
  'professional_title_override',
  'affiliations_override',
  'phone_override',
  'location_override',
  'allow_tracking',   // Exhibitor Leads opt-in
  'agree_to_tc',      // Terms & Conditions placeholder
]

Trusted Staff and above:

[
  'pronouns_override',
  'full_name_override',
  'professional_title_override',
  'affiliations_override',
  'email_override',
  'phone_override',
  'location_override',
  'badge_type_code_override',   // + badge_type_override (text label)
  'registration_type_code_override',  // + registration_type_override (text label)
  'option_1' ... 'option_8',    // i.e. other_1_code ... other_8_code
  'ticket_1_code' ... 'ticket_8_code',
  'allow_tracking',
  'agree_to_tc',
  'hide',
  'priority',
  'notes',
]

Administratorcan_edit_fields = ['*'] (all fields)

Badge type options (hardcoded for now): member, non-member, guest, exhibitor, staff, test (In future: read from Event Badge Template's configured list)

Registration type options: Same list as badge type for now — identical select options.

Future: Per-Event Configuration

event.mod_badges_json.edit_permissions — placeholder settings UI exists in ae_comp__event_settings_badges_form.svelte. Review page uses hardcoded defaults for now. The settings form and review page are not yet connected.

{
  "authenticated": {
    "can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
  },
  "trusted": {
    "can_edit": ["*attendee_fields", "email_override", "badge_type_code_override", "registration_type_code_override", "option_x", "ticket_x_code", "allow_tracking", "agree_to_tc", "hide", "priority", "notes"]
  }
}

Search & Filter Capabilities

Search Component

File: ae_comp__badge_search.svelte

Multi-Word Search Fix (2026-02-26)

Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:

// "scott idem" → LIKE '%scott%' AND LIKE '%idem%'
// Previously: LIKE '%scott idem%' (failed to match)
const words = qry.split(/\s+/).filter(w => w.length > 0);
for (const word of words) {
    search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${word}%` });
}

Committed: dc0f3066

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 Button (Implemented 2026-02-26)

The handle_print_badge() function in ae_comp__badge_obj_view.svelte increments the count and records timestamps:

async function handle_print_badge() {
    const now = new Date().toISOString();
    const current_print_count = $lq__event_badge_obj.print_count ?? 0;
    const data_to_update = {
        print_count: current_print_count + 1,
        print_last_datetime: now
    };
    if (current_print_count === 0) {
        data_to_update.print_first_datetime = now;  // Only set on first print
    }
    await events_func.update_ae_obj__event_badge({ ... });
}

Button has data-testid="badge-print-btn" and shows loading/done/error states with icon feedback.

Print Workflow

  1. Pre-Print: Badge print page (/print) shows "Already printed N times" warning in screen-only header if print_count >= 1
  2. Record: handle_print_badge() updates print_count, print_last_datetime, and print_first_datetime (first print only) via API before printing
  3. Print: window.print() — standard browser print dialog, wired and working (2026-02-27)
  4. Redirect: After 1 second, goto(/events/{id}/badges) returns to search
  5. Audit: print_first_datetime and print_last_datetime visible in Edit Mode debug row

Browser vs Electron: Badge printing does NOT require the Electron native app. The standard browser print dialog works well across Chrome, Chromium, and Firefox. The Electron native app is specialized for the Events Pres Mgmt Launcher only and should not be assumed available for badge stations.


Database Schema

IndexedDB Table: badge

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

Indexed Fields:

badge: `
  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,
  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 (results)
├── ae_comp__badge_create_form.svelte   # (Not actively used)
├── ae_comp__badge_upload_form.svelte   # Bulk CSV upload
└── [badge_id]/
    ├── ae_comp__badge_obj_view.svelte  # Badge rendering + staff edit + print button
    ├── ae_comp__badge_review_form.svelte  # Form-based field review/edit (attendee + staff)
    ├── print/
    │   ├── +page.ts                    # Non-blocking badge loader (inc_template: true)
    │   └── +page.svelte                # Print-focused page — screen header + badge render
    └── review/
        ├── +page.ts                    # Non-blocking badge loader (inc_template: false)
        └── +page.svelte                # Passcode-gated review page

Note: The old [badge_id]/+page.svelte placeholder was removed (2026-02-27). The name link in the search results list now goes directly to /print.

Badge Print Page (/print)

  • Screen-only header (print:hidden): "Back to Search" link + "Already printed N times" warning
  • Badge rendered via ae_comp__badge_obj_view with is_review_mode={false}
  • Print button inside ae_comp__badge_obj_view handles count update → window.print() → redirect to search
  • Page <title> includes badge name + event name

Badge Review Page (/review)

  • Passcode-gated for attendees — URL ?passcode=... matched against badge.person_passcode
    • Note: person_passcode field is not yet in the DB (as of 2026-02-27). Review page accessible to staff via trusted_access without a passcode.
  • Access hierarchy (checked in order):
    1. Administrator → full access (can_edit_fields = ['*'])
    2. Trusted Staff → staff field set
    3. Attendee with valid passcode → attendee field set
    4. No access → passcode entry form shown
  • Uses ae_comp__badge_review_form.svelte (NOT badge render)
  • "Back to Search" link shown for staff only

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 edit button (or #review URL hash for future self-service)
  • Condition: Renders only when BOTH $lq__event_badge_obj AND $lq__event_badge_template_obj are non-null
  • 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
  • data-testid attributes: badge-edit-btn, badge-save-btn, badge-cancel-btn, badge-print-btn, badge-professional-title-input — use these in tests

Testing Status

Current Test Coverage

  • Badge list loads (all 6 data integrity tests passing)
  • Badge template list loads and displays
  • Badge template form renders and populates correctly
  • Badge template values persist in edit form
  • Electron bridge compatibility (graceful degradation in browser)
  • Badge field processor handles missing optional fields
  • Badge type filter tests
  • Badge template relationship tests
  • Attendee workflow test — navigate → edit professional title → print → return (d1ded2d4)

Key Test Lessons Learned

Search API path is FLAT, not nested. search_ae_obj builds /v3/crud/{obj_type}/search — always flat regardless of the parent relationship. Mocks must match this:

// CORRECT — flat path
url.includes('/v3/crud/event_badge/search') && method === 'POST'
// WRONG — nested path, mock will never fire
url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST'

List API (GET) is also FLAT with query params. get_ae_obj_li builds /v3/crud/{obj_type}/?for_obj_id=... — always flat. Mocks must check url.includes('/v3/crud/event_badge_template/') && url.includes('for_obj_id').

CSS input[value*=...] selectors don't work with Svelte bind:value. The CSS selector checks the HTML attribute; Svelte's bind:value sets the DOM property only. In Playwright tests, use page.getByLabel() or locator.inputValue() instead.

Dexie requires _random ID fields. Badge objects saved to IDB must include:

event_badge_id_random: string  // Must be present or Dexie skips the object
id_random: string              // Also checked
// Error: "Object is missing a valid ID for table 'badge'"

All API mock responses in tests need these fields.

Badge view requires both badge AND template. ae_comp__badge_obj_view.svelte wraps everything in {#if $lq__event_badge_obj && $lq__event_badge_template_obj} — if the template isn't loaded, edit/print buttons and the badge itself don't render. Tests must mock the badge template endpoint.

Badge GET endpoint (single object): /v3/crud/event_badge/{id} (NOT nested under event). Matches api.get_ae_obj() which uses the flat path.

Badge PATCH endpoint (update): /v3/crud/event/${event_id}/event_badge/${badge_id} (nested under event). Matches api.patch_ae_obj() which uses the nested path.

Use data-testid for test selectors. Key buttons have targets: badge-edit-btn, badge-save-btn, badge-cancel-btn, badge-print-btn, badge-professional-title-input.

Remaining Test Issues

None — all current badge tests passing as of 2026-02-26 (f5e98b8c).


Known Issues & Future Enhancements

Known Issues

  1. Session Cold-Start: Potential race condition on first load (same as pres mgmt module)
  2. Type Definitions: Some pre-existing TypeScript errors on external package types (not introduced by badge work)
  3. person_passcode not in DB: Attendee-gated review URL (?passcode=...) cannot function until this field is added to the event_badge schema. The review page falls back to passcode entry form for non-staff.
  4. Print page CSS: Badge print rendering and @page print styles not yet fine-tuned — expected to need work
  5. mod_badges_json.edit_permissions not connected: Settings UI exists but review page uses hardcoded field defaults

Implemented (2026-02-27)

  • window.print() wired to print button (records count first, then prints, then redirects)
  • Dedicated /print page — replaces old [badge_id]/+page.svelte placeholder
  • Dedicated /review page — passcode-gated, access-tiered
  • ae_comp__badge_review_form.svelte — stub created, full form fields pending
  • Badge search results visibility rules (unprinted-only for non-edit, all for trusted+edit)
  • Badge list: 4 action buttons per row (Print, Review nav, Copy Link, Email Link) — all Lucide icons
  • Print page: 3 action buttons in header (Print Now, Review nav, Email Link) — all Lucide icons
  • Review page: 3 action buttons in header (Print nav, Copy Link, Email Link) — all Lucide icons
  • Print button: not shown when already printed (unless Edit Mode)
  • Print count shown as Nx badge next to printer icon
  • Email obscuring for non-trusted users
  • Email Review Link button (placeholder alert — email API pending)
  • Direct Review Link clipboard copy (trusted + Edit Mode only)
  • Fixed: components no longer write to $ae_loc.edit_mode
  • Settings UI for edit_permissions per event (ae_comp__event_settings_badges_form.svelte)
  • All badge module icons converted to Lucide (Font Awesome removed from badge routes)

Recently Completed (2026-02-27)

  • Badge Review Formae_comp__badge_review_form.svelte fully implemented (fields, QR, save/cancel, options/tickets, accessibility, help modal)
  • Print font size controls (v1) — Screen-only []/[+]/[↺] panel on print page; 4 px props added to ae_comp__badge_obj_view.svelte; auto-sizing unchanged when props absent
  • Bug fixdefault_authenticated_fields / default_trusted_fields in review/+page.svelte corrected (wrong field names caused silent save drops)

Still Needed — HIGH PRIORITY (first show: April 2026)

Still Needed — MEDIUM PRIORITY

  1. Email API for review links: send_review_email() is a placeholder alert(). Needs actual email send endpoint.
  2. person_passcode DB field: Add to event_badge schema to enable attendee-gated review URLs.
  3. Connect edit_permissions config: Read mod_badges_json.edit_permissions in review page instead of hardcoded defaults.
  4. Print page CSS / @page styles: Badge rendering, sizing, and print-specific stylesheet.

Still Needed — FUTURE / LOW PRIORITY

  1. Batch Operations: Bulk update, bulk print, bulk export
  2. Audit Log: Track who edited which fields and when
  3. Photo Badges: Support badge photo upload and display
  4. Real-Time Sync: WebSocket updates for multi-device badge printing stations

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: 🔄 In Progress Last Verified: 2026-02-27 (rev 5 — field permissions spec added, header buttons implemented, review form fields pending) Verified Against: Code as of 2026-02-27 (branch ae_app_3x_llm)