# 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 (rev 3) --- ## 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:** ```typescript // 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:** ```python 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:** ```typescript // 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` ```json { "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` ### 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: ```typescript // "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) ```typescript // 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' ``` ### QR Scan Search - 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()` ```typescript 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 ```typescript // 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:** ```typescript 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: ```typescript load_ae_obj_id__event_badge({ event_badge_id: badge_id, inc_template: true // Also loads template }) ``` --- ## Print Tracking ### Print Fields ```typescript 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: ```typescript 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:** Check `print_count` to warn if already printed 2. **Print:** `window.print()` — standard browser print dialog. Works well in Chrome, Chromium, and Firefox. Chrome is the recommended browser for onsite badge printing (most stable in production). 3. **Post-Print:** Handled by `handle_print_badge()` — count + timestamps updated 4. **Audit:** Print history available for staff review **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. **Current Status:** Button records print event and updates IDB. The `window.print()` call still needs to be wired to the button — see Future Enhancements. --- ## Database Schema ### IndexedDB Table: `badge` **File:** `src/lib/ae_events/db_events.ts` (Lines 841-852) **Indexed Fields:** ```typescript 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` ```typescript // 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_random` → `event_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 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_v3` builds `/v3/crud/{obj_type}/search` — always flat regardless of the parent relationship. Mocks must match this: ```typescript // 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_v3` 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: ```typescript 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_v3()` 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_v3()` 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. **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 `window.print()` to the print button. Standard browser print API — works well in Chrome/Chromium/Firefox for badge label printing. Electron is NOT needed. 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 ```typescript // 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 ```typescript // 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) ``` --- ## Related Documentation - [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md) - [Development Guide](./GUIDE__Development.md) - [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md) - [Naming Conventions](./AE__Naming_Conventions.md) --- **Document Status:** ✅ Complete **Last Verified:** 2026-02-26 (rev 3 — all badge tests passing) **Verified Against:** Code commit f5e98b8c (all data integrity tests passing)