diff --git a/documentation/MODULE__AE_Events_Badges.md b/documentation/MODULE__AE_Events_Badges.md new file mode 100644 index 00000000..242fcd42 --- /dev/null +++ b/documentation/MODULE__AE_Events_Badges.md @@ -0,0 +1,586 @@ +# 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:** +```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 | Staff only | +| `badge_type_code` | `badge_type_code_override` | Badge access level code | 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` + +### 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. + +// 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 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:** +```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 `#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 +```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 +**Verified Against:** Code commit b28595da (badge data fixes)