From 5c3823f41a96a37f224ada25df95490a16af6f31 Mon Sep 17 00:00:00 2001
From: Scott Idem
Date: Mon, 2 Mar 2026 19:47:11 -0500
Subject: [PATCH] feat(badges): implement badge print controls panel and refine
badge overrides
- Create ae_comp__badge_print_controls.svelte: A fixed-right-edge panel for per-field accordion controls, font size adjustments, and inline editing.
- Refactor print/+page.svelte to integrate the new controls panel and standardize font size state management via $bindable() props.
- Update ae_comp__badge_obj_view.svelte and ae_comp__badge_review_form.svelte to correctly sync badge_type_code_override and badge_type_override.
- Improve badge_type_name derivation logic to prioritize staff overrides.
- Hide unused receipt/ticket sections in badge view pending future redesign.
- Update documentation (PROJECT and TODO) to reflect completion of Task 3.
---
.../PROJECT__AE_Events_Badges_Review_Print.md | 98 ++-
documentation/TODO__Agents.md | 22 +-
.../css/badge_layout_epson_4x5_fanfold.css | 3 +
.../css/badge_layout_zebra_zc10l_pvc.css | 3 +
.../[badge_id]/ae_comp__badge_obj_view.svelte | 127 ++--
.../ae_comp__badge_print_controls.svelte | 611 ++++++++++++++++++
.../ae_comp__badge_review_form.svelte | 23 +-
.../badges/[badge_id]/print/+page.svelte | 153 ++---
8 files changed, 873 insertions(+), 167 deletions(-)
create mode 100644 src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte
diff --git a/documentation/PROJECT__AE_Events_Badges_Review_Print.md b/documentation/PROJECT__AE_Events_Badges_Review_Print.md
index 09f167a9..f744b40d 100644
--- a/documentation/PROJECT__AE_Events_Badges_Review_Print.md
+++ b/documentation/PROJECT__AE_Events_Badges_Review_Print.md
@@ -1,15 +1,96 @@
# PROJECT: AE Events Badges — Review Form & Print Font Controls
**Created:** 2026-02-27
-**Last Updated:** 2026-02-27
+**Last Updated:** 2026-03-02
**Branch:** `ae_app_3x_llm`
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
**Owner:** Scott Idem / One Sky IT
-**Status:** ✅ TASK 1 (Badge Review Form) COMPLETE | ✅ TASK 2 (Print Font Controls) COMPLETE — v1
+**Status:** ✅ TASK 1 COMPLETE | ✅ TASK 2 COMPLETE | ✅ TASK 3 COMPLETE | ⏳ TASK 4 NEXT UP
---
-## Implementation Status (2026-02-27)
+## Next Up for Badges (TASK 4)
+
+### 1. QR Code on Badge Front — `ae_comp__badge_obj_view.svelte`
+The badge template has a `show_qr` flag (or similar). When toggled on, the QR code should
+appear on the front face of the printed badge. Currently QR is only shown on the review form.
+
+- Check the badge template fields for the QR toggle field name (`show_qr`, `qr_enabled`, etc.)
+ via `ae_describe event_badge_template` and inspect `ae_comp__badge_obj_view.svelte`.
+- The QR code data URL is generated with:
+ ```typescript
+ qr_data_url = await core_func.js_generate_qr_code('obj', {
+ obj_type: 'event_badge',
+ obj_id: event_badge_id
+ });
+ ```
+ See `ae_comp__badge_review_form.svelte` for the working pattern.
+- Position on badge: typically bottom-right corner of the badge face, sized to fit within
+ the template's layout constraints. Do NOT alter structural badge dimensions.
+- Must be hidden on `ae_comp__badge_obj_view.svelte` when `show_qr` is falsy.
+
+### 2. Badge Print Controls — UX Improvements (ae_comp__badge_print_controls.svelte)
+- Scott has identified areas for improvement — TBD next session
+- Consider: keyboard shortcuts (+ / -) for font sizing while a field is active
+- Consider: "Apply to all badges" workflow for font size presets
+
+### 3. Leads Module
+Next major work after badge polish. See `documentation/MODULE__AE_Events_Leads.md` (if it
+exists) for context. Exhibitor lead scanning via QR code.
+
+---
+
+## Implementation Status
+
+### ✅ TASK 3: Badge Print Controls Panel — COMPLETE (2026-03-02)
+
+**Files created/modified:**
+- `ae_comp__badge_print_controls.svelte` — NEW. Right-edge control panel with per-field
+ accordion sections. Font size controls + inline edit forms gated by access level.
+- `print/+page.svelte` — layout changed from `flex-row` to fixed right panel.
+
+**Design decisions:**
+- Controls panel is `position: fixed right-0 top-20 bottom-0 w-64` — out of normal flow,
+ always visible regardless of viewport width. `top-20` (80px) clears the page header.
+- Badge area gets `pr-64` to prevent content from hiding under the fixed panel.
+ `print:pr-0` and `print:hidden` on the panel restore a clean print layout.
+- `bg-white dark:bg-zinc-900` gives the panel a solid background to prevent bleed-through.
+
+**Per-field accordion structure (one open at a time):**
+
+| Field | Access | Font Controls |
+| --- | --- | --- |
+| Name | Trusted+ edit | ✅ |
+| Professional Title | All auth edit | ✅ |
+| Affiliations | All auth edit (textarea) | ✅ |
+| Location | All auth edit | ✅ |
+| Lead Scanning (allow_tracking) | All auth edit | — |
+| Pronouns | Trusted+ edit | — |
+| Badge Type | Trusted+, only when template has badge_type_list | — |
+
+**Access level note:**
+`is_trusted = $derived($ae_loc.trusted_access === true)` — covers Trusted, Administrator,
+Manager, Super (cascade). No need to OR in `administrator_access`.
+
+**badge_type_override coupling:**
+When badge type is changed via dropdown, both `badge_type_code_override` AND
+`badge_type_override` are saved together (name comes from template list). Same behavior in
+`ae_comp__badge_obj_view.svelte` and `ae_comp__badge_review_form.svelte`.
+Edge case: custom names (e.g. code=`member`, name=`"Life Member"`) must be set manually in DB.
+
+**Font size config (moved from print page to controls component):**
+
+| Field | Default px | Range | Step |
+|--------------|------------|----------|------|
+| Name | 58px | 20–80px | 2px |
+| Title | 34px | 14–56px | 2px |
+| Affiliations | 38px | 14–60px | 2px |
+| Location | 34px | 14–56px | 2px |
+
+Font sizes flow back to the parent via `$bindable()` props so `ae_comp__badge_obj_view`
+stays in sync without prop-drilling through a third component.
+
+---
### ✅ TASK 1: Badge Review Form — COMPLETE
@@ -245,6 +326,13 @@ This allows for rich text formatting (bold, italic, line breaks, etc.) in badge
|---|---|---|
| `email_override` | email input | Fallback display: `email` |
| `badge_type_code_override` | select | Options: member, non-member, guest, exhibitor, staff, test; also updates `badge_type_override` text |
+
+> **Edge case — custom badge type name:** If an attendee needs a standard badge type code (for
+> CSS styling) but a slightly different displayed name (e.g. code=`member`, name=`"Life Member"`),
+> set `badge_type_override` directly in the DB. Do **not** use the dropdown — selecting from the
+> dropdown in the UI overwrites `badge_type_override` with the standard name from the template
+> list. This is an intentional trade-off: coded for the normal case (dropdown keeps both fields in
+> sync), special cases handled manually by Scott in the DB.
| `registration_type_code_override` | select | Same options as badge_type for now; also updates `registration_type_override` |
| `hide` | checkbox | Label: "Hidden from search results" |
| `priority` | number input | |
@@ -380,7 +468,9 @@ $ae_loc.administrator_access // true = administrator and above
$ae_loc.edit_mode // boolean — user preference toggle (NEVER write to this from components)
```
-`is_staff` prop on the review form = `administrator_access || trusted_access`.
+`is_staff` prop on the review form = `$ae_loc.trusted_access`.
+`trusted_access` is `true` for Trusted and every level above it (Administrator, Manager, Super)
+— no need to OR in `administrator_access` since it's already implied by the cascade.
---
diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md
index 7a043189..470f5dbe 100644
--- a/documentation/TODO__Agents.md
+++ b/documentation/TODO__Agents.md
@@ -8,17 +8,37 @@
- [ ] **Step 3:** Implement formal error boundaries for 403/401 API responses to provide user-friendly "Session Expired" or "Access Denied" UI.
## 🚧 Upcoming High Priority
+
+### [Badges] Remaining badge work before first live event
+- **QR code on badge front:** `ae_comp__badge_obj_view.svelte` — display QR on the printed
+ face when template has `show_qr` (or equivalent) toggled on. Use same QR generation as
+ review form (`core_func.js_generate_qr_code`). See TASK 4 in `PROJECT__AE_Events_Badges_Review_Print.md`.
+- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
+ File: `ae_comp__badge_print_controls.svelte`.
+
+### [Leads] Exhibitor Lead Scanning — NEXT MAJOR FEATURE
+QR code scan at exhibitor booth → capture attendee badge data. Gated by `allow_tracking` on
+the badge. Check if `documentation/MODULE__AE_Events_Leads.md` exists for full spec.
+Key questions before starting: which routes, does the Electron app scan, what does the
+lead record look like in the DB?
+
+### [General]
- **CRUD v2 Refactor:** Finalize retirement of `Element_ae_crud_v2.svelte` in favor of V3 Editor.
- **Temp Cleanup:** Auto-removal of native `.tmp` files older than 24h.
- **`window.print()` for badge print button:** Wire the existing `handle_print_badge()` to trigger `window.print()`. Browser print works well across Chrome/Chromium/Firefox — no Electron needed.
- **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.)
## ✅ Completed Recently
+- [x] **[Badges]** **Badge Print Controls Panel:** New `ae_comp__badge_print_controls.svelte` — per-field accordion with inline edit forms, font size controls, access-level gating. Fixed-right-edge layout replaces collapsed `flex-1` panel. (2026-03-02, branch `ae_app_3x_llm`)
+- [x] **[Badges]** **badge_type_override coupling:** Selecting badge type from dropdown now saves both `badge_type_code_override` AND `badge_type_override` in `ae_comp__badge_obj_view.svelte`, `ae_comp__badge_review_form.svelte`, and `ae_comp__badge_print_controls.svelte`.
+- [x] **[Badges]** **Layout CSS system:** `data-layout` attribute, `@page` injection, `style_href` for per-template CSS files. Two templates: `badge_layout_epson_4x5_fanfold.css`, `badge_layout_zebra_zc10l_pvc.css`.
+- [x] **[Badges]** **Duplex field wiring:** Badge back hidden for single-sided templates.
+- [x] **[Badges]** **Badge Review Form:** Complete with QR code, field edits, access-level gating, accessibility toggle, help modal. (`ae_comp__badge_review_form.svelte`)
- [x] **[API]** **V3 Lookup System Integration:** Implemented standardized `/v3/lookup/` endpoints for Countries, Subdivisions, and Time Zones. Added support for `only_priority` filtering in IDAA editors.
- [x] **[UI]** **Events Launcher Location Fix:** Resolved room select list issues by ensuring all enabled/hidden locations are proactively loaded and synced.
- [x] **[API]** **Event File V3 Mapping:** Implemented `inc_hosted_file` support and mapped prefixed backend fields (`hosted_file_hash_sha256`, etc.) to flat properties.
- [x] **[UI]** **Badge Rendering Fix:** Refactored `badge_template` lookup to use V3 Triple ID pattern.
-- [x] **[API]** **event_session Search Fix:** Resolved 400 error (`Unauthorized search field 'account_id'`) via backend update.
+- [x] **[API]** **event_session Search Fix:** Resolved 400 error (`Unauthorized search field ‘account_id’`) via backend update.
- [x] **[Security]** Purged redundant `x-aether-api-token` from frontend and notified backend.
- [x] **[Security]** Fixed misplaced `Access-Control-Allow-Origin` request headers.
- [x] **[Security]** Implemented "Account ID Scavenging" to fix hydration race conditions.
diff --git a/src/lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css b/src/lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css
index e086784b..a5436d8d 100644
--- a/src/lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css
+++ b/src/lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css
@@ -22,6 +22,9 @@
width: 4in;
min-height: 5in;
max-height: 5in;
+
+ /* debug */
+ /* outline: thick solid orange; */
}
/* Body area: 5in total − ~1in header − ~0.5in footer = ~3.5in for content */
diff --git a/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css b/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css
index 3bd1c2c3..8e0d1892 100644
--- a/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css
+++ b/src/lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css
@@ -20,6 +20,9 @@
width: 3.5in;
min-height: 5.5in;
max-height: 5.5in;
+
+ /* debug */
+ /* outline: thick solid orange; */
}
/* Body area: 5.5in total − ~1in header − ~0.5in footer = ~4in for content.
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
index f56c1a58..e8fac23d 100644
--- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
+++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte
@@ -26,10 +26,10 @@
update_status = $bindable('idle'),
update_complete = $bindable(true),
is_review_mode = false,
- font_size_name = undefined,
- font_size_title = undefined,
- font_size_affiliations = undefined,
- font_size_location = undefined,
+ font_size_name = 48,
+ font_size_title = 24,
+ font_size_affiliations = 24,
+ font_size_location = 24,
log_lvl = 0
}: Props = $props();
@@ -84,12 +84,42 @@
}
});
+ // Human-readable name for the current badge type, printed on the badge footer.
+ // Priority: badge_type_override (staff custom name) → badge_type (base import name)
+ // → template badge_type_list lookup by effective code → code itself as last resort.
+ //
+ // Why separate from code: the code is the CSS class hook for per-event stylesheets.
+ // A special-case attendee can share a code (and thus styling) with "Member" but have a
+ // custom displayed name like "Life Member" stored in badge_type_override.
+ let badge_type_name = $derived.by(() => {
+ // Staff override name takes priority
+ const override_name = $lq__event_badge_obj?.badge_type_override;
+ if (override_name) return override_name;
+ // Base name from import/registration system
+ const base_name = $lq__event_badge_obj?.badge_type;
+ if (base_name) return base_name;
+ // Fall back to template list lookup using the effective code
+ const found = (badge_type_code_li as { code: string; name: string }[])
+ .find(item => item.code === editable_badge_type_code);
+ return found?.name ?? editable_badge_type_code ?? '';
+ });
+
// Show the badge back section when duplex=1 (or when duplex is not yet set, as a safe default).
// duplex=0/false → single-sided print (e.g. Zebra ZC10L PVC cards); back section is hidden.
let show_badge_back = $derived(
$lq__event_badge_template_obj?.duplex == null || !!$lq__event_badge_template_obj?.duplex
);
+ let show_receipt = $derived(() => {
+ // return $lq__event_badge_template_obj?.show_receipt;
+ return false; // Receipt section is currently disabled pending redesign, as the current layout is not working well.
+ });
+
+ let show_tickets = $derived(() => {
+ // return $lq__event_badge_template_obj?.show_tickets;
+ return false; // Ticket section is currently disabled pending redesign, as the current layout is not working well.
+ });
+
// *** Set initial variables
$effect(() => {
$slct.event_badge_id = event_badge_id; // $page['page_for']['event_badge_id'];
@@ -142,7 +172,9 @@
editable_allow_tracking =
$lq__event_badge_obj.allow_tracking ?? null;
editable_email = $lq__event_badge_obj.email ?? null;
+ // Use staff override code if set; base code from import/registration otherwise.
editable_badge_type_code =
+ $lq__event_badge_obj.badge_type_code_override ??
$lq__event_badge_obj.badge_type_code ?? null;
// Only set the local edit state — never touch $ae_loc.edit_mode here.
@@ -430,8 +462,21 @@
if (editable_email !== $lq__event_badge_obj.email) {
data_to_update.email = editable_email;
}
- if (editable_badge_type_code !== $lq__event_badge_obj.badge_type_code) {
- data_to_update.badge_type_code = editable_badge_type_code;
+ // Compare against the effective code (override ?? base) to detect a real change.
+ // Staff edits go to badge_type_code_override — the base import code is never overwritten.
+ const stored_effective_code =
+ $lq__event_badge_obj.badge_type_code_override ??
+ $lq__event_badge_obj.badge_type_code;
+ if (editable_badge_type_code !== stored_effective_code) {
+ data_to_update.badge_type_code_override = editable_badge_type_code;
+ // Keep badge_type_override in sync — look up the name from the template list.
+ // Edge case: if a badge needs a custom name that differs from the template list
+ // (e.g. "Life Member" for a "member" code), set badge_type_override manually
+ // in the DB. Do not use the dropdown — it will overwrite the custom name.
+ data_to_update.badge_type_override =
+ (badge_type_code_li as { code: string; name: string }[])
+ .find(item => item.code === editable_badge_type_code)?.name
+ ?? editable_badge_type_code;
}
if (Object.keys(data_to_update).length === 0) {
@@ -483,6 +528,7 @@
$lq__event_badge_obj.allow_tracking ?? null;
editable_email = $lq__event_badge_obj.email ?? null;
editable_badge_type_code =
+ $lq__event_badge_obj.badge_type_code_override ??
$lq__event_badge_obj.badge_type_code ?? null;
}
if (!is_review_mode) {
@@ -565,6 +611,13 @@ onkeypress={() => {
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte
index 579f8272..80dd5053 100644
--- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte
+++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte
@@ -226,6 +226,18 @@
}
}
+ // Name lookup for the hardcoded badge type options in this form.
+ // Must stay in sync with the