fix(badges): wire badge_type_list from template instead of hardcoded ISHLT list

Replace static `badge_type_code_li` array (hardcoded ISHLT 2024 types) with
`$derived.by()` that parses `$lq__event_badge_template_obj.badge_type_list` JSON.
Each event's template defines its own badge type set — the component now reflects
that correctly. Falls back to `[]` on missing/invalid JSON (hides the type select).

Also add MODULE__AE_Events_Badge_Templates.md documenting template field reference,
external CSS approach, layout codes, duplex field plan, and standard template setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-02 15:43:49 -05:00
parent 9939d94970
commit 827c7ac62e
2 changed files with 289 additions and 17 deletions

View File

@@ -0,0 +1,276 @@
# MODULE: Aether Events — Badge Templates
**Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
**Database Table:** `event_badge_template`
**Last Updated:** 2026-03-02
---
## Overview
Badge templates define the visual and structural configuration for printing event badges.
Each template applies to one category of badge (e.g., general attendees, workshops,
exhibitors). An event typically has 13 templates.
**Key principle:** One template per badge stock type/audience. Do not use flags on a
single template to drive multiple layouts — create a separate template instead.
**Common template sets:**
- **General Attendees** — main conference badge (most attendees)
- **Workshops / Pre-conference** — alternate header, possibly different badge type list
- **Exhibitors** — distinct footer stripe colors, exhibitor-specific badge types
Each template uses the same physical badge stock and printer configuration.
---
## DB Field Reference
### Core Identity
| Field | Type | Notes |
|---|---|---|
| `id` | int | Internal PK |
| `id_random` | str | External-facing ID (AE Triple ID pattern) |
| `event_id` | str | Parent event |
| `name` | str | Template display name |
| `description` | str | Optional description |
### Image Assets
| Field | Type | Notes |
|---|---|---|
| `logo_path` | str (URL) | Org logo — fallback when no header image |
| `logo_filename` | str | **Deprecated** — redundant with logo_path; do not use |
| `header_path` | str (URL) | Front-of-badge header image — primary branding |
| `secondary_header_path` | str (URL) | Back-of-badge header image (falls back to header_path) |
| `footer_path` | str (URL) | Optional footer image — rarely used |
| `header_row_1` | str/HTML | Text fallback line 1 when no header image |
| `header_row_2` | str/HTML | Text fallback line 2 |
| `footer_title`, `footer_left`, `footer_right` | str | Legacy Flask-era fields — not used |
| `header_background`, `footer_background` | str | Legacy — not used; do not add to new templates |
### Network / WiFi
| Field | Type | Notes |
|---|---|---|
| `wireless_ssid` | str | WiFi network name — displayed on badge back |
| `wireless_password` | str | WiFi password — displayed on badge back |
### QR Code Behavior
| Field | Type | Notes |
|---|---|---|
| `show_qr_front` | bool (0/1) | Show attendee QR code on front of badge |
| `show_qr_back` | bool (0/1) | Show attendee QR code (+ ID text) on back of badge |
### Badge Type List
| Field | Type | Notes |
|---|---|---|
| `badge_type_list` | JSON string | List of `{code, name}` objects for this template |
**Format:**
```json
[
{"code": "current_member", "name": "Member"},
{"code": "guest", "name": "Guest"},
{"code": "staff", "name": "Staff"},
{"code": "test", "name": "Test"}
]
```
The badge type footer stripe color is driven by CSS rules targeting the `code` value
as a class on the footer element. Each event/template defines its own list — there is
no global default. The component derives this list from the template at render time.
### Ticket Definitions
| Field | Type | Notes |
|---|---|---|
| `ticket_list` | JSON string | List of `{num, code, name}` for this template's tickets |
| `ticket_1_text` `ticket_8_text` | HTML | Ticket block HTML printed on badge back |
**ticket_list format:**
```json
[
{"num": 1, "code": "foundation_reception", "name": "Foundation Reception"},
{"num": 2, "code": "volunteer_reception", "name": "Volunteer Reception"}
]
```
The `ticket_N_code` field on the badge object references a ticket by its `code`. The
corresponding `ticket_N_text` on the template provides the HTML rendered on the badge.
### Print Layout / Styling
| Field | Type | Notes |
|---|---|---|
| `layout` | str | Layout code — see Layout Codes below |
| `style_filename` | str | CSS filename for locally-served stylesheets |
| `style_href` | str (URL) | **Preferred** — external URL for custom CSS |
| `script_src` | str (URL) | **Do not use** — Flask-era arbitrary script injection |
### Access Control
| Field | Type | Notes |
|---|---|---|
| `passcode` | str | Shared passcode for template management access |
| `enable` | bool | Standard AE enable flag |
| `hide` | bool | Standard AE hide flag |
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
| `notes` | str | Internal notes |
### New Field (pending backend addition)
| Field | Type | Notes |
|---|---|---|
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
The `duplex` field controls whether the back-of-badge section renders during printing.
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
prints. The back section still displays on screen for configuration reference.
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
---
## External CSS Approach
### Why External
Badge templates may need visual adjustments mid-event (e.g., a color correction, a
footer fix) without deploying a new SvelteKit build. Hosting the CSS at an external URL
allows changes to take effect on next page load without any deployment.
### How It Works
The `style_href` field contains a full URL to a CSS file hosted on the static server
(e.g., `https://static.oneskyit.com/c/ISHLT/css/badges_custom_ishlt.css`).
The print page (`print/+page.svelte`) or the badge view should conditionally add a
`<link>` element via `<svelte:head>` when `style_href` is populated:
```svelte
<svelte:head>
{#if $lq__event_badge_template_obj?.style_href}
<link
rel="stylesheet"
href={$lq__event_badge_template_obj.style_href}
/>
{/if}
</svelte:head>
```
This is not yet implemented — tracked as a pending Phase 1 item.
### CSS Scope
External badge CSS should scope all rules under `.badge_front`, `.badge_back`, etc.
to avoid bleeding into the rest of the app. The classes used in
`ae_comp__badge_obj_view.svelte` are the canonical hook points:
- `.badge_front` — entire front card
- `.badge_back` — entire back card
- `.badge_header` — front header area
- `.badge_body` — front content area (name, title, affiliations, location)
- `.badge_footer` — front footer stripe
- `.badge_back_header` — back header area
- `.badge_back_content` — back content area
- `.badge_footer_center.<code>` — footer text per badge type code (for color stripes)
### layout field
The `layout` field encodes physical badge stock dimensions. Standard codes to use:
| Code | Dimensions | Description |
|---|---|---|
| `badge_3.5x5.5_pvc` | 3.5" × 5.5" (88.9 × 139.7mm) | PVC card, Zebra ZC10L |
| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | Fanfold paper badge |
| `badge_4x6_fanfold_tickets` | 4" × 6" + tear-offs | Fanfold with ticket stubs |
The current badge component is hard-coded to `w-[4in]` / `min-h-[6.0in]`. These
dimensions will need to be made template-driven once the `layout` field is wired up.
For now, override via print CSS in the `style_href` stylesheet.
---
## Template-Derived Features (component behavior)
### badge_type_list → badge type select
The badge type dropdown shown when editing a badge is derived from the template's
`badge_type_list` JSON, not a hardcoded list. This was a bug (fixed 2026-03-02).
See `ae_comp__badge_obj_view.svelte``badge_type_code_li` is now `$derived.by()`.
### "Info section" flags (exhibitor_info, presenter_info, etc.)
These flags (`exhibitor_info`, `presenter_info`, `staff_info`, `vip_info`, `vote_info`)
**do not exist as DB columns**. They appeared as placeholder `{#if}` blocks in the
badge view component from Flask-era development and were never implemented.
The correct approach is **one template per badge audience** — an Exhibitor template will
have exhibitor-specific `badge_type_list`, header images, and CSS. No flags needed.
The dead `{#if $lq__event_badge_template_obj.exhibitor_info}` blocks in
`ae_comp__badge_obj_view.svelte` should be removed in a future cleanup pass.
---
## Properties Saved to IDB (Dexie)
The `properties_to_save` array in `ae_events__event_badge_template.ts` controls what
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
in DB and may be needed:
- `style_href` — needed once external CSS is wired via `<svelte:head>`
- `passcode` — not needed client-side
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
- `header_background`, `footer_background` — not needed (legacy)
- `script_src` — do not add; this field should not be used
- `duplex`**add when backend adds the field**
---
## Standard Template Setup (per event)
### 1. General Attendees template
- `header_path`: event-specific conference header image
- `secondary_header_path`: back-of-badge header (often same or related image)
- `wireless_ssid` + `wireless_password`: venue WiFi
- `show_qr_back`: `1` (back QR is standard for most events)
- `show_qr_front`: `0` (usually off for front)
- `badge_type_list`: full list of member/guest/staff/test types
- `ticket_list` + `ticket_N_text`: event-specific tickets if applicable
- `style_href`: client-specific CSS URL
- `layout`: appropriate layout code
- `duplex`: `1` (or `0` for single-sided events like Axonius 2026)
### 2. Workshop / Pre-conference template
- Same as above but with workshop-specific header images
- `badge_type_list`: reduced list (workshop-relevant types only)
- `ticket_list`: may be empty `[]`
- `duplex`: match main template
### 3. Exhibitor template
- Exhibitor-specific header images
- `badge_type_list`: exhibitor-only types (`ex_all`, `ex_booth`, `guest`, `staff`, `test`)
- `ticket_list`: `[]` (exhibitors typically don't have event tickets)
- `show_qr_back`: may be `0` (exhibitors scan others, they don't need their own QR prominent)
- `wireless_ssid` + `wireless_password`: same venue WiFi
---
## Related Files
| File | Role |
|---|---|
| `ae_events__event_badge_template.ts` | API + IDB functions; `properties_to_save` |
| `db_events.ts` | Dexie schema for `badge_template` table |
| `templates/+page.svelte` | Template list + create/edit/delete UI |
| `templates/ae_comp__badge_template_form.svelte` | Template create/edit form |
| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render — consumes template data |
| `[badge_id]/print/+page.svelte` | Print page — loads template, hosts `<svelte:head>` CSS |
| `documentation/MODULE__AE_Events_Badges.md` | Badge object reference |
---
## Pending / TODO
- [ ] Wire `style_href` via `<svelte:head>` in print page
- [ ] Add `duplex` to `properties_to_save` once backend field exists
- [ ] Add `duplex`-driven `print:hidden` to `badge_back` section in `ae_comp__badge_obj_view.svelte`
- [ ] Make `layout` field drive actual card dimensions in the badge component (currently hard-coded)
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte`
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)

View File

@@ -65,22 +65,18 @@
// let ae_tmp: key_val = $state({});
let ae_triggers: key_val = $state({});
// ISHLT 2024 badge type codes
let badge_type_code_li = [
{ code: 'current_member', name: 'Member' },
{ code: 'inactive_member', name: 'Non-Member' },
{ code: 'current_member_trainee', name: 'Trainee Member' },
{ code: 'inactive_member_trainee', name: 'Trainee Non-Member' },
{ code: 'ex_all', name: 'Exhibitor All Access' },
{ code: 'ex_booth', name: 'Exhibitor Booth Staff' },
{ code: 'hftx', name: 'HFTX Master Academy' },
{ code: 'mcs', name: 'MCS Master Academy' },
{ code: 'pediatric', name: 'Pediatric' },
{ code: 'guest', name: 'Guest' },
{ code: 'staff', name: 'Staff' },
{ code: 'volunteer', name: 'Volunteer' },
{ code: 'test', name: 'Test' }
];
// Badge type list is derived from the template — each event/template defines its own set.
// Falls back to empty array if template not loaded or badge_type_list is invalid JSON.
let badge_type_code_li = $derived.by(() => {
const raw = $lq__event_badge_template_obj?.badge_type_list;
if (!raw) return [];
try {
return JSON.parse(raw);
} catch {
console.warn('badge_type_list is not valid JSON:', raw);
return [];
}
});
// *** Set initial variables
$effect(() => {
@@ -999,7 +995,7 @@ onkeypress={() => {
"
title={editable_badge_type_code}
>
{#if edit_mode_active && badge_type_code_li}
{#if edit_mode_active && badge_type_code_li.length > 0}
<label
>Badge Type:
<select