feat(badges): cfg_json body_text_color applied in renderer

This commit is contained in:
Scott Idem
2026-04-08 12:32:13 -04:00
parent 56b4e5c627
commit b02843e467
4 changed files with 147 additions and 5 deletions

View File

@@ -293,6 +293,73 @@ Frontend guidance:
---
## Axonius Zoom CSV Upload (Temporary — Apr 2026)
Purpose: Staff-only quick upload to upsert Event Person + Event Badge records from a Zoom Events registrant CSV.
- **Endpoint:** `POST /event/{event_id}/badge/import/zoom_csv`
- **Auth:** include `x-aether-api-key` (if required) and account context via `x-account-id: <ACCOUNT_ID>`. Admin bypass (`x-no-account-id: bypass`) or `?jwt=<token>` are accepted per site policy.
- **Request:** `multipart/form-data` with single file field `file` (Zoom CSV). Query params:
- `begin_at` (int, default `0`)
- `end_at` (int, default `20000`)
- `return_detail` (bool, default `false`)
- Delimiter is auto-detected; Zoom CSV layout: row 1 = metadata, row 2 = blank, row 3 = headers (the backend skips the first two rows).
Behavior / notes:
- The handler forces `Registrant email` to be used as the `external_id`. `Unique identifier` is used as `external_registration_id` only when it is meaningful (placeholders like `N/A`, `NA`, `UNKNOWN` are ignored).
- Per-ticket custom fields are parsed (Organization, Job title, Phone, Address lines, City, State/Province, Postal/Zip, Country, etc.).
- Marketing-consent values are mapped to `agree_to_tc` and `allow_tracking`.
- TEMP AXONIUS MAPPING: the import temporarily defaults `event_badge_template_id` to `21` and `event_badge_template_id_random` to `RKYp2HcQm9o`. Ticket-name → `badge_type_code` mapping is applied for some labels (e.g., contains "sponsor" → `sponsor`; contains "attend"/"attendee" → `attendee`). This mapping is temporary (April 2026) — surface this to staff.
- Rows missing `Registrant email` are skipped.
- The server upserts via existing backend methods and creates/updates `event_person`, `event_person_profile`, and `event_badge` records as needed.
Frontend guidance:
- UI must be staff-only and should validate an `event_id` is selected.
- For large files, use `begin_at`/`end_at` to process in chunks.
- Prefer `return_detail=false` for large imports to reduce payload size.
Common errors:
- `403` — missing/invalid account context or API key.
- `404` — event not found.
- `500` — file save or processing error.
Example curl (replace placeholders):
```bash
curl -v -X POST "https://api.example.com/event/<EVENT_ID>/badge/import/zoom_csv?begin_at=0&end_at=20000&return_detail=false" \
-H "x-aether-api-key: <API_KEY>" \
-H "x-account-id: <ACCOUNT_ID>" \
-F "file=@/path/to/zoom_export.csv"
```
Sample success (summary mode, `return_detail=false`):
```json
{
"data": [
{
"event_id": "xK9mP3qRtL2",
"event_id_random": "xK9mP3qRtL2",
"external_id": "alice@example.com",
"given_name": "Alice",
"family_name": "Smith",
"email": "alice@example.com"
}
],
"meta": {
"status_code": 200,
"status_name": "OK",
"success": true,
"data_type": "list",
"data_list_count": 1
}
}
```
Sample success (detailed, `return_detail=true`) — `data` contains full `event_person` objects with nested `event_badge` (may include temporary `event_badge_template_id`: `21` and `event_badge_template_id_random`: `RKYp2HcQm9o`).
Paste this section into the guide as a temporary Axonius-specific note (April 2026). Consider linking staff to a sample Zoom CSV for QA.
---
## 7. User Actions (`/v3/action/user/`)
Stateful user account operations that are not standard CRUD. All require `x-aether-api-key`.

View File

@@ -0,0 +1,35 @@
// Type definition for badge template cfg_json stored on templates.
export interface BadgeTemplateCfg {
hide_badge_header?: boolean;
hide_badge_footer?: boolean;
show_qr_front?: boolean;
show_qr_back?: boolean;
// Per-field hide toggles
hide_title?: boolean;
hide_affiliations?: boolean;
hide_location?: boolean;
// Alignment overrides
align?: {
name?: string;
title?: string;
affiliations?: string;
location?: string;
};
// QR alignment
qr_alignment?: {
front?: string;
back?: string;
};
// Layout fit height overrides
fit_heights?: Record<string, string>;
// Body text color: either a Tailwind `text-*` class or a hex color like `#112233`.
body_text_color?: string;
// Allow arbitrary extra keys to preserve forward-compatibility.
[key: string]: any;
}

View File

@@ -92,6 +92,7 @@ import {
Utensils,
Wifi
} from '@lucide/svelte';
import type { BadgeTemplateCfg } from '$lib/ae_events/types/ae_badge_template_cfg';
// --- Badge type list from template ---
// Each item: { code: string, name: string }. Drives footer display + (in controls) dropdown.
let badge_type_code_li = $derived.by(() => {
@@ -198,9 +199,9 @@ let template_cfg = $derived.by(() => {
const raw = $lq__event_badge_template_obj?.cfg_json;
if (!raw) return {};
try {
return typeof raw === 'string' ? JSON.parse(raw) : raw;
return typeof raw === 'string' ? (JSON.parse(raw) as BadgeTemplateCfg) : (raw as BadgeTemplateCfg);
} catch {
return {};
return {} as BadgeTemplateCfg;
}
});
@@ -294,6 +295,31 @@ let qr_back_justify = $derived.by(() => {
return map[val] ?? 'center';
});
// Body text color: can be a Tailwind `text-*` class or a hex value like `#112233`.
let body_text_color_class = $derived.by(() => {
const cfg = template_cfg || {};
const raw = cfg?.body_text_color ?? cfg?.text_color ?? '';
if (!raw || typeof raw !== 'string') return '';
const v = raw.trim();
if (v.startsWith('text-')) return v;
// Map simple color names to Tailwind where reasonable (e.g., 'white' -> 'text-white')
if (/^[a-zA-Z]+$/.test(v)) {
const pick = v.toLowerCase();
const allowed = ['white','black','gray','red','blue','green','yellow','indigo','purple','pink'];
if (allowed.includes(pick)) return `text-${pick}`;
}
return '';
});
let body_text_color_style = $derived.by(() => {
const cfg = template_cfg || {};
const raw = cfg?.body_text_color ?? cfg?.text_color ?? '';
if (!raw || typeof raw !== 'string') return '';
const v = raw.trim();
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v)) return `color: ${v};`;
return '';
});
/**
* Layout-aware section heights for Element_fit_text.
*
@@ -660,9 +686,10 @@ const code_to_icon: {
items-center
justify-end overflow-clip
p-0 px-8 pb-1
text-white
{body_text_color_class || 'text-white'}
gap-0
">
"
style="{body_text_color_style}">
<!--
person_name container: explicit height from fit_heights so Element_fit_text
can measure overflow correctly. flex-col with justify-content distributes

View File

@@ -4,6 +4,7 @@ import { Loader2 } from '@lucide/svelte';
import type { key_val } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { ae_api } from '$lib/stores/ae_stores';
import type { BadgeTemplateCfg } from '$lib/ae_events/types/ae_badge_template_cfg';
interface Props {
event_id: string;
@@ -56,6 +57,8 @@ let cfg_show_qr_back = $state(true);
let cfg_hide_title = $state(false);
let cfg_hide_affiliations = $state(false);
let cfg_hide_location = $state(false);
// Body text color: Tailwind class (e.g. 'text-black') or hex (e.g. '#000000')
let cfg_body_text_color = $state('text-black');
// Alignment overrides: 'left' | 'center' | 'right' | 'justify'
let cfg_align_name = $state('center');
let cfg_align_title = $state('center');
@@ -125,6 +128,9 @@ async function load_template(id: string) {
cfg_hide_affiliations = parsed_cfg.hasOwnProperty('hide_affiliations') ? !!parsed_cfg.hide_affiliations : false;
cfg_hide_location = parsed_cfg.hasOwnProperty('hide_location') ? !!parsed_cfg.hide_location : false;
// Body text color
cfg_body_text_color = parsed_cfg.body_text_color ?? parsed_cfg.text_color ?? 'text-white';
// Alignment overrides (nested under cfg_json.align and cfg_json.qr_alignment)
cfg_align_name = parsed_cfg?.align?.name ?? parsed_cfg.align_name ?? 'center';
cfg_align_title = parsed_cfg?.align?.title ?? parsed_cfg.align_title ?? 'center';
@@ -152,7 +158,7 @@ async function handle_submit() {
submit_status = 'loading';
// Merge cfg_json preserving unknown keys, then set our cfg flags
let cfg_obj: any = {};
let cfg_obj: BadgeTemplateCfg = {};
try {
cfg_obj = existing_cfg_raw
? typeof existing_cfg_raw === 'string'
@@ -184,6 +190,9 @@ async function handle_submit() {
cfg_obj.qr_alignment.front = cfg_qr_alignment_front;
cfg_obj.qr_alignment.back = cfg_qr_alignment_back;
// Body text color (Tailwind class or hex)
cfg_obj.body_text_color = cfg_body_text_color;
const data_to_save: key_val = {
name,
background_image_path,
@@ -409,6 +418,10 @@ function handle_cancel() {
<option value="justify">Justify</option>
</select>
</label>
<label class="label">
<span>Body Text Color (Tailwind class or #hex)</span>
<input type="text" bind:value={cfg_body_text_color} class="input" placeholder="text-black or #000000" />
</label>
</div>
<p class="text-xs text-surface-400 italic">
These values are saved into <code>cfg_json</code>. Existing cfg_json keys are preserved.