feat: badge print controls — per-template field visibility and edit access via other_json.controls_cfg
Add field_shown() and field_editable() functions driven by event_badge_template.other_json:
controls_cfg: { shown?: string[], auth_editable?: string[] }
Access rules:
- No authenticated_access → display-only, no edit buttons shown
- authenticated only → can edit fields in auth_editable (default: title/affiliations/location/allow_tracking/pronouns)
- trusted + edit_mode → always sees and edits all fields, ignores config
Each attendee field card (name, title, affiliations, location, allow_tracking, pronouns)
is now wrapped in {#if field_shown()} and its edit button/accordion gated by field_editable().
No backend changes needed — other_json is an existing longtext JSON column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,9 +86,9 @@
|
||||
// --- Access level ---
|
||||
// trusted_access is true for Trusted and every level above it (Administrator,
|
||||
// Manager, Super). No need to OR in administrator_access — it's already covered.
|
||||
// Basic authenticated users (trusted_access === false) can only edit
|
||||
// professional_title_override, affiliations_override, and location_override.
|
||||
let is_trusted = $derived($ae_loc.trusted_access === true);
|
||||
// Minimum bar to edit any field. Anonymous/public users see values but cannot edit.
|
||||
let is_auth = $derived($ae_loc.authenticated_access === true);
|
||||
|
||||
// IMPORTANT: $ae_loc.edit_mode is the GLOBAL AE Edit Mode — a UI preference that
|
||||
// reveals editable fields, debug info, and advanced options across the whole app.
|
||||
@@ -98,6 +98,46 @@
|
||||
// — used here to allow reprinting an already-printed badge when global edit mode is active.
|
||||
let is_global_edit_mode = $derived($ae_loc.edit_mode === true);
|
||||
|
||||
// --- Per-template controls config ---
|
||||
// Stored in event_badge_template.other_json as { controls_cfg: { shown?, auth_editable? } }.
|
||||
// shown: array of field keys to render; omit/null = all shown.
|
||||
// auth_editable: fields editable by authenticated (non-trusted) users; omit/null = defaults below.
|
||||
// trusted + edit_mode always overrides config — sees and edits all fields.
|
||||
// WHY: allows locking down a production template so attendees can only touch the
|
||||
// fields the event coordinator explicitly permits, without touching the backend.
|
||||
interface ControlsCfg { shown?: string[]; auth_editable?: string[]; }
|
||||
let template_controls_cfg = $derived((() => {
|
||||
try {
|
||||
const parsed = JSON.parse($lq__event_badge_template_obj?.other_json ?? '{}');
|
||||
return (parsed?.controls_cfg ?? null) as ControlsCfg | null;
|
||||
} catch { return null; }
|
||||
})());
|
||||
|
||||
// Default auth-editable fields when the template has no explicit config.
|
||||
// Covers the fields an attendee most commonly needs to fix at the badge table.
|
||||
const DEFAULT_AUTH_EDITABLE = ['title', 'affiliations', 'location', 'allow_tracking', 'pronouns'];
|
||||
|
||||
/** Is this field card shown in the panel at all? trusted+edit always sees all fields. */
|
||||
function field_shown(field: string): boolean {
|
||||
if (is_trusted && is_global_edit_mode) return true;
|
||||
const cfg = template_controls_cfg;
|
||||
if (!cfg?.shown) return true;
|
||||
return cfg.shown.includes(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the current user edit this field?
|
||||
* - No authenticated_access → never (display only).
|
||||
* - trusted + edit_mode → always.
|
||||
* - authenticated only → field must be in template's auth_editable list (or defaults).
|
||||
*/
|
||||
function field_editable(field: string): boolean {
|
||||
if (!is_auth) return false;
|
||||
if (is_trusted && is_global_edit_mode) return true;
|
||||
const auth_editable = template_controls_cfg?.auth_editable ?? DEFAULT_AUTH_EDITABLE;
|
||||
return auth_editable.includes(field);
|
||||
}
|
||||
|
||||
// --- Section collapse state ---
|
||||
// Attendee fields start open (primary use); staff starts closed to save space.
|
||||
// Toggling one collapses the other (auto-collapse, same pattern as launcher sections).
|
||||
@@ -597,7 +637,9 @@
|
||||
|
||||
<!-- === NAME ===
|
||||
Font controls: always visible (all can adjust for readability at kiosk).
|
||||
Edit form: Trusted+ only — accordion opens only when is_trusted. -->
|
||||
Edit form: gated by field_editable('name') — by default trusted+edit only;
|
||||
can be opened to authenticated users via template controls_cfg.auth_editable. -->
|
||||
{#if field_shown('name')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'name'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -605,10 +647,10 @@
|
||||
{#if get_display('full_name_override', 'full_name')}
|
||||
<p class="font-bold leading-snug truncate">{get_display('full_name_override', 'full_name')}</p>
|
||||
{:else}
|
||||
<p class="text-gray-400 italic text-xs">{is_trusted ? 'Tap ✎ to add' : 'Not set'}</p>
|
||||
<p class="text-gray-400 italic text-xs">{field_editable('name') ? 'Tap ✎ to add' : 'Not set'}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if is_trusted}
|
||||
{#if field_editable('name')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
@@ -623,9 +665,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
{@render font_ctrl('name')}
|
||||
<!-- Accordion drawer: grid-template-rows 0fr→1fr animates height smoothly.
|
||||
Always in DOM, only opens if is_trusted (edit button present above). -->
|
||||
<div class="ctrl-accordion" class:open={active_field === 'name' && is_trusted}>
|
||||
<!-- Accordion drawer: grid-template-rows 0fr→1fr animates height smoothly. -->
|
||||
<div class="ctrl-accordion" class:open={active_field === 'name' && field_editable('name')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-name" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
{#if is_global_edit_mode}
|
||||
@@ -655,8 +696,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: name -->
|
||||
|
||||
<!-- === PROFESSIONAL TITLE === -->
|
||||
{#if field_shown('title')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'title'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -667,20 +710,22 @@
|
||||
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('title')}
|
||||
aria-expanded={active_field === 'title'}
|
||||
aria-controls="field-form-title"
|
||||
title="Edit professional title"
|
||||
aria-label="Edit professional title"
|
||||
>
|
||||
{#if active_field === 'title'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{#if field_editable('title')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('title')}
|
||||
aria-expanded={active_field === 'title'}
|
||||
aria-controls="field-form-title"
|
||||
title="Edit professional title"
|
||||
aria-label="Edit professional title"
|
||||
>
|
||||
{#if active_field === 'title'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{@render font_ctrl('title')}
|
||||
<div class="ctrl-accordion" class:open={active_field === 'title'}>
|
||||
<div class="ctrl-accordion" class:open={active_field === 'title' && field_editable('title')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-title" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
{#if is_global_edit_mode}
|
||||
@@ -708,8 +753,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: title -->
|
||||
|
||||
<!-- === AFFILIATIONS === -->
|
||||
{#if field_shown('affiliations')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'affiliations'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -720,20 +767,22 @@
|
||||
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('affiliations')}
|
||||
aria-expanded={active_field === 'affiliations'}
|
||||
aria-controls="field-form-affiliations"
|
||||
title="Edit affiliations"
|
||||
aria-label="Edit affiliations"
|
||||
>
|
||||
{#if active_field === 'affiliations'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{#if field_editable('affiliations')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('affiliations')}
|
||||
aria-expanded={active_field === 'affiliations'}
|
||||
aria-controls="field-form-affiliations"
|
||||
title="Edit affiliations"
|
||||
aria-label="Edit affiliations"
|
||||
>
|
||||
{#if active_field === 'affiliations'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{@render font_ctrl('affiliations')}
|
||||
<div class="ctrl-accordion" class:open={active_field === 'affiliations'}>
|
||||
<div class="ctrl-accordion" class:open={active_field === 'affiliations' && field_editable('affiliations')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-affiliations" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
{#if is_global_edit_mode}
|
||||
@@ -761,8 +810,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: affiliations -->
|
||||
|
||||
<!-- === LOCATION === -->
|
||||
{#if field_shown('location')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'location'}>
|
||||
<div class="flex items-center gap-2 px-2 pt-1 pb-0.5">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -773,20 +824,22 @@
|
||||
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('location')}
|
||||
aria-expanded={active_field === 'location'}
|
||||
aria-controls="field-form-location"
|
||||
title="Edit location"
|
||||
aria-label="Edit location"
|
||||
>
|
||||
{#if active_field === 'location'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{#if field_editable('location')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('location')}
|
||||
aria-expanded={active_field === 'location'}
|
||||
aria-controls="field-form-location"
|
||||
title="Edit location"
|
||||
aria-label="Edit location"
|
||||
>
|
||||
{#if active_field === 'location'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{@render font_ctrl('location')}
|
||||
<div class="ctrl-accordion" class:open={active_field === 'location'}>
|
||||
<div class="ctrl-accordion" class:open={active_field === 'location' && field_editable('location')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-location" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
{#if is_global_edit_mode}
|
||||
@@ -814,8 +867,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: location -->
|
||||
|
||||
<!-- === ALLOW TRACKING (Lead Scanning) === -->
|
||||
{#if field_shown('allow_tracking')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'allow_tracking'}>
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -824,19 +879,21 @@
|
||||
{$lq__event_badge_obj?.allow_tracking ? 'Allowed' : 'Not allowed'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('allow_tracking')}
|
||||
aria-expanded={active_field === 'allow_tracking'}
|
||||
aria-controls="field-form-allow-tracking"
|
||||
title="Edit lead scanning preference"
|
||||
aria-label="Edit lead scanning"
|
||||
>
|
||||
{#if active_field === 'allow_tracking'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{#if field_editable('allow_tracking')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('allow_tracking')}
|
||||
aria-expanded={active_field === 'allow_tracking'}
|
||||
aria-controls="field-form-allow-tracking"
|
||||
title="Edit lead scanning preference"
|
||||
aria-label="Edit lead scanning"
|
||||
>
|
||||
{#if active_field === 'allow_tracking'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ctrl-accordion" class:open={allow_tracking_open}>
|
||||
<div class="ctrl-accordion" class:open={allow_tracking_open && field_editable('allow_tracking')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-allow-tracking" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
@@ -898,8 +955,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: allow_tracking -->
|
||||
|
||||
<!-- === PRONOUNS === -->
|
||||
{#if field_shown('pronouns')}
|
||||
<div class="field-card rounded-lg overflow-hidden" class:field-card--active={active_field === 'pronouns'}>
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -910,19 +969,21 @@
|
||||
<p class="text-gray-400 italic text-xs">Tap ✎ to add</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('pronouns')}
|
||||
aria-expanded={active_field === 'pronouns'}
|
||||
aria-controls="field-form-pronouns"
|
||||
title="Edit pronouns"
|
||||
aria-label="Edit pronouns"
|
||||
>
|
||||
{#if active_field === 'pronouns'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{#if field_editable('pronouns')}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs preset-tonal-primary shrink-0 transition-colors"
|
||||
onclick={() => toggle_field('pronouns')}
|
||||
aria-expanded={active_field === 'pronouns'}
|
||||
aria-controls="field-form-pronouns"
|
||||
title="Edit pronouns"
|
||||
aria-label="Edit pronouns"
|
||||
>
|
||||
{#if active_field === 'pronouns'}<ChevronDown size="12" />{:else}<Pencil size="12" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ctrl-accordion" class:open={active_field === 'pronouns'}>
|
||||
<div class="ctrl-accordion" class:open={active_field === 'pronouns' && field_editable('pronouns')}>
|
||||
<div class="ctrl-accordion-inner">
|
||||
<div id="field-form-pronouns" class="px-2 pb-2 pt-1.5 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||
<input
|
||||
@@ -947,6 +1008,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}<!-- field_shown: pronouns -->
|
||||
|
||||
</div><!-- ctrl-accordion-inner attendee -->
|
||||
</div><!-- ctrl-accordion attendee -->
|
||||
|
||||
Reference in New Issue
Block a user