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={() => {

Click the button to generate the QR code.

{/if} --> + +
+ {$lq__event_badge_template_obj?.name ?? '—'} + | + {$lq__event_badge_template_obj?.layout ?? '(no layout)'} +
+
{ max-w-[8.5in] overflow-visible " > - {#if $lq__event_badge_obj && $lq__event_badge_template_obj} @@ -920,41 +966,25 @@ onkeypress={() => { {/if} - {#if editable_email || edit_mode_active} + {#if edit_mode_active} + {/if} - {#if editable_allow_tracking !== null || edit_mode_active} -
- {#if edit_mode_active} - - {:else} - Allow Tracking: {editable_allow_tracking - ? 'Yes' - : 'No'} - {/if} + {#if edit_mode_active} + +
+
{/if}
@@ -1023,7 +1053,6 @@ onkeypress={() => { {:else} - THE FOOTER {#if option_other_1_override && ['front_bool', 'front_back_bool'].includes(option_other_1_display_opt)} { > {/if} + {editable_badge_type_code}{badge_type_name} {#if option_other_2_override && ['front_bool', 'front_back_bool'].includes(option_other_2_display_opt)} @@ -1427,6 +1458,7 @@ onkeypress={() => { + {#if show_receipt} + {/if} + {#if show_tickets} + {/if} {/if} diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte new file mode 100644 index 00000000..b50d799f --- /dev/null +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_print_controls.svelte @@ -0,0 +1,611 @@ + + + + + +{#snippet font_ctrl(key: 'name' | 'title' | 'affiliations' | 'location')} + {@const cur = + key === 'name' ? font_size_name : + key === 'title' ? font_size_title : + key === 'affiliations' ? font_size_affiliations : font_size_location} +
+ Font + + + {cur !== null ? `${cur}px` : 'Auto'} + + + {#if cur !== null} + + {/if} +
+{/snippet} + + +{#snippet field_actions(field_key: string, on_save: () => void, on_cancel: () => void)} +
+ + +
+{/snippet} + + +
+ + + +
+
+
+

Name

+ {#if get_display('full_name_override', 'full_name')} +

{get_display('full_name_override', 'full_name')}

+ {:else} +

Not set{is_trusted ? ' — tap ✎ to add' : ''}

+ {/if} +
+ {#if is_trusted} + + {/if} +
+ {@render font_ctrl('name')} + {#if active_field === 'name' && is_trusted} +
+ + + {@render field_actions( + 'name', + () => save_field('name', { full_name_override: edit_full_name_override || null }), + () => cancel_field('name') + )} +
+ {/if} +
+ + + +
+
+
+

Professional Title

+ {#if get_display('professional_title_override', 'professional_title')} +

{get_display('professional_title_override', 'professional_title')}

+ {:else} +

Not set — tap ✎ to add

+ {/if} +
+ +
+ {@render font_ctrl('title')} + {#if active_field === 'title'} +
+ + + {@render field_actions( + 'title', + () => save_field('title', { professional_title_override: edit_professional_title_override || null }), + () => cancel_field('title') + )} +
+ {/if} +
+ + + +
+
+
+

Affiliations

+ {#if get_display('affiliations_override', 'affiliations')} +

{get_display('affiliations_override', 'affiliations')}

+ {:else} +

Not set — tap ✎ to add

+ {/if} +
+ +
+ {@render font_ctrl('affiliations')} + {#if active_field === 'affiliations'} +
+ + + {@render field_actions( + 'affiliations', + () => save_field('affiliations', { affiliations_override: edit_affiliations_override || null }), + () => cancel_field('affiliations') + )} +
+ {/if} +
+ + + +
+
+
+

Location

+ {#if get_display('location_override', 'location')} +

{get_display('location_override', 'location')}

+ {:else} +

Not set — tap ✎ to add

+ {/if} +
+ +
+ {@render font_ctrl('location')} + {#if active_field === 'location'} +
+ + + {@render field_actions( + 'location', + () => save_field('location', { location_override: edit_location_override || null }), + () => cancel_field('location') + )} +
+ {/if} +
+ + + +
+
+
+

Lead Scanning

+

+ {$lq__event_badge_obj?.allow_tracking ? 'Allowed' : 'Not allowed'} +

+
+ +
+ {#if active_field === 'allow_tracking'} +
+ + {@render field_actions( + 'allow_tracking', + () => save_field('allow_tracking', { allow_tracking: edit_allow_tracking }), + () => cancel_field('allow_tracking') + )} +
+ {/if} +
+ + + {#if is_trusted} + + +
+
+
+

Pronouns

+ {#if get_display('pronouns_override', 'pronouns')} +

{get_display('pronouns_override', 'pronouns')}

+ {:else} +

Not set — tap ✎ to add

+ {/if} +
+ +
+ {#if active_field === 'pronouns'} +
+ + + {@render field_actions( + 'pronouns', + () => save_field('pronouns', { pronouns_override: edit_pronouns_override || null }), + () => cancel_field('pronouns') + )} +
+ {/if} +
+ + + + {#if badge_type_code_li.length > 0} +
+
+
+

Badge Type

+ {#if badge_type_display} +

{badge_type_display}

+ {:else} +

Not set — tap ✎ to assign

+ {/if} +
+ +
+ {#if active_field === 'badge_type'} +
+ + + {@render field_actions( + 'badge_type', + () => save_field('badge_type', { + badge_type_code_override: edit_badge_type_code, + // Keep badge_type_override in sync (name from template list). + // See edge-case note in PROJECT__AE_Events_Badges_Review_Print.md. + badge_type_override: edit_badge_type_code + ? (badge_type_code_li.find(item => item.code === edit_badge_type_code)?.name ?? edit_badge_type_code) + : null + }), + () => cancel_field('badge_type') + )} +
+ {/if} +
+ {/if} + + {/if} + +
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