From b02843e467b4a49a5bbddf97310e3a895e5e0787 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 8 Apr 2026 12:32:13 -0400 Subject: [PATCH] feat(badges): cfg_json body_text_color applied in renderer --- .../GUIDE__AE_API_V3_for_Frontend.md | 67 +++++++++++++++++++ .../ae_events/types/ae_badge_template_cfg.ts | 35 ++++++++++ .../[badge_id]/ae_comp__badge_obj_view.svelte | 35 ++++++++-- .../ae_comp__badge_template_form.svelte | 15 ++++- 4 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 src/lib/ae_events/types/ae_badge_template_cfg.ts diff --git a/documentation/GUIDE__AE_API_V3_for_Frontend.md b/documentation/GUIDE__AE_API_V3_for_Frontend.md index b7e32d29..5b005a3b 100644 --- a/documentation/GUIDE__AE_API_V3_for_Frontend.md +++ b/documentation/GUIDE__AE_API_V3_for_Frontend.md @@ -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: `. Admin bypass (`x-no-account-id: bypass`) or `?jwt=` 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//badge/import/zoom_csv?begin_at=0&end_at=20000&return_detail=false" \ + -H "x-aether-api-key: " \ + -H "x-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`. diff --git a/src/lib/ae_events/types/ae_badge_template_cfg.ts b/src/lib/ae_events/types/ae_badge_template_cfg.ts new file mode 100644 index 00000000..e8dbb287 --- /dev/null +++ b/src/lib/ae_events/types/ae_badge_template_cfg.ts @@ -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; + + // 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; +} 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 dadb5de2..0789a8eb 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 @@ -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}">