feat(badges): cfg_json body_text_color applied in renderer
This commit is contained in:
@@ -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`.
|
||||
|
||||
35
src/lib/ae_events/types/ae_badge_template_cfg.ts
Normal file
35
src/lib/ae_events/types/ae_badge_template_cfg.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user