# 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: `Dr. Jane Smith` or `Chief Medical Officer` - Line breaks: `Hospital Name
Department Name` - Special characters and entities **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 with HTML badge.full_name_override = "Bob 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 // Shows: Bob 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 "Bob 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:** ```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) — 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`):** ```typescript 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 | `` | | Print (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | ``, 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) | `` | | 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 | `` 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):** ```typescript [ '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:** ```typescript [ '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', ] ``` **Administrator** — `can_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. ```json { "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: ```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:** 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:** ```typescript 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` ```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 (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 `` 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: ```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` 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()` 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 Form** — `ae_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 fix** — `default_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 ```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:** 🔄 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)