Badges: per-badge locked font sizes via cfg_json

Allows coordinators to pre-tune font sizes for attendees with long names
and have those sizes apply automatically on every kiosk, not just one machine.

- ae_types.ts: add cfg_json to ae_EventBadge interface
- db_events.ts: add cfg_json to Badge Dexie interface
- ae_events__event_badge.ts: add cfg_json to properties_to_save so it is
  persisted to IndexedDB on load and returned by the API
- print/+page.svelte: on first load per badge, read cfg_json.font_sizes and
  initialize font_size_name/title/affiliations/location state from saved values
  (guarded by _font_sizes_loaded_for to avoid clobbering user adjustments on
  background liveQuery refreshes)
- ae_comp__badge_print_controls.svelte: add lock_font_sizes() and
  reset_font_sizes_to_auto() functions; add Lock Sizes / Auto reset UI in the
  Staff adjustments section (trusted-only); button shows warning style when
  sizes are unsaved vs success when locked; status indicator shows what is
  currently locked

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-09 21:47:34 -04:00
parent 941ad6ae88
commit c9e2284758
5 changed files with 184 additions and 0 deletions

View File

@@ -632,6 +632,7 @@ export const properties_to_save = [
'print_last_datetime',
'allow_tracking',
'agree_to_tc',
'cfg_json',
'other_1_code',
'other_2_code',
'other_3_code',

View File

@@ -170,6 +170,7 @@ export interface Badge {
// passcode?: null|string;
cfg_json?: null | string;
// data_json?: null|string;
default_qry_str?: null | string;

View File

@@ -507,6 +507,7 @@ export interface ae_EventBadge extends ae_BaseObj {
agree_to_tc?: boolean | null;
ticket_list?: any[] | null;
cfg_json?: any;
data_json?: any;
default_qry_str?: string | null;
}

View File

@@ -624,6 +624,113 @@ let is_dirty_badge_type = $derived(
null)
);
// --- Lock / Reset font sizes to badge cfg_json ---
// "Lock Sizes" saves the current font_size_* values into event_badge.cfg_json.font_sizes.
// Any kiosk that opens this badge will then load those sizes instead of auto-sizing.
// "Reset to Auto" saves null for all fields, restoring auto-sizing everywhere.
// Trusted-only — attendees at the badge table can't permanently alter the layout.
type LockStatus = 'idle' | 'saving' | 'done' | 'error';
let lock_sizes_status: LockStatus = $state('idle');
// True when at least one size is non-null (i.e. user has adjusted something)
let has_any_size_override = $derived(
font_size_name !== null ||
font_size_title !== null ||
font_size_affiliations !== null ||
font_size_location !== null
);
// Detect whether current state differs from what is saved in badge cfg_json
// so we can highlight the button when there are unsaved size changes.
let saved_font_sizes = $derived.by(() => {
try {
const cfg = typeof $lq__event_badge_obj?.cfg_json === 'string'
? JSON.parse($lq__event_badge_obj.cfg_json)
: ($lq__event_badge_obj?.cfg_json ?? {});
return cfg?.font_sizes ?? null;
} catch {
return null;
}
});
let sizes_are_dirty = $derived(
font_size_name !== (saved_font_sizes?.name ?? null) ||
font_size_title !== (saved_font_sizes?.title ?? null) ||
font_size_affiliations !== (saved_font_sizes?.affiliations ?? null) ||
font_size_location !== (saved_font_sizes?.location ?? null)
);
async function lock_font_sizes() {
if (!$lq__event_badge_obj?.event_badge_id) return;
lock_sizes_status = 'saving';
try {
// Merge into existing cfg_json — preserve any other keys that may be there
const existing_cfg = (() => {
try {
return typeof $lq__event_badge_obj.cfg_json === 'string'
? JSON.parse($lq__event_badge_obj.cfg_json)
: ($lq__event_badge_obj.cfg_json ?? {});
} catch { return {}; }
})();
const new_cfg = {
...existing_cfg,
font_sizes: {
name: font_size_name,
title: font_size_title,
affiliations: font_size_affiliations,
location: font_size_location
}
};
await events_func.update_ae_obj__event_badge({
api_cfg: $ae_api,
event_id,
event_badge_id: $lq__event_badge_obj.event_badge_id,
data_kv: { cfg_json: JSON.stringify(new_cfg) },
log_lvl
});
lock_sizes_status = 'done';
setTimeout(() => { lock_sizes_status = 'idle'; }, 1500);
} catch (err) {
console.error('Badge print controls: lock font sizes error:', err);
lock_sizes_status = 'error';
setTimeout(() => { lock_sizes_status = 'idle'; }, 2500);
}
}
async function reset_font_sizes_to_auto() {
// Clear all local size state
font_size_name = null;
font_size_title = null;
font_size_affiliations = null;
font_size_location = null;
if (!$lq__event_badge_obj?.event_badge_id) return;
// Persist the reset back to the badge — removes the locked sizes server-side
lock_sizes_status = 'saving';
try {
const existing_cfg = (() => {
try {
return typeof $lq__event_badge_obj.cfg_json === 'string'
? JSON.parse($lq__event_badge_obj.cfg_json)
: ($lq__event_badge_obj.cfg_json ?? {});
} catch { return {}; }
})();
const new_cfg = { ...existing_cfg };
delete new_cfg.font_sizes;
await events_func.update_ae_obj__event_badge({
api_cfg: $ae_api,
event_id,
event_badge_id: $lq__event_badge_obj.event_badge_id,
data_kv: { cfg_json: Object.keys(new_cfg).length ? JSON.stringify(new_cfg) : null },
log_lvl
});
lock_sizes_status = 'done';
setTimeout(() => { lock_sizes_status = 'idle'; }, 1500);
} catch (err) {
console.error('Badge print controls: reset font sizes error:', err);
lock_sizes_status = 'error';
setTimeout(() => { lock_sizes_status = 'idle'; }, 2500);
}
}
// TC modal ref for the lead scanning terms & conditions dialog
let tc_dialog_ref: HTMLDialogElement | undefined;
@@ -1476,6 +1583,50 @@ let allow_tracking_open = $derived(
</label>
</div>
<!-- === LOCK FONT SIZES ===
Saves the current font_size_* values into event_badge.cfg_json.font_sizes
so they survive page reloads and apply on every kiosk, not just this machine.
Use this for attendees with long names that auto-sizing can't handle well.
Trusted-only. -->
<div class="space-y-1 px-1 pt-1">
<p class="field-label px-1">Locked Font Sizes</p>
<div class="flex gap-1.5">
<button
type="button"
class="btn btn-xs flex-1 transition-colors {lock_sizes_status === 'done' ? 'preset-filled-success' : lock_sizes_status === 'error' ? 'preset-tonal-error' : lock_sizes_status === 'saving' ? 'preset-tonal-surface' : sizes_are_dirty ? 'preset-filled-warning' : 'preset-tonal-surface'}"
disabled={lock_sizes_status === 'saving' || !has_any_size_override}
onclick={lock_font_sizes}
title="Save current font sizes to this badge record — applies on all kiosks">
{#if lock_sizes_status === 'saving'}
<LoaderCircle size="11" class="mr-1 animate-spin" /> Saving…
{:else if lock_sizes_status === 'done'}
<Check size="11" class="mr-1" /> Locked
{:else if lock_sizes_status === 'error'}
Error
{:else}
🔒 Lock Sizes
{/if}
</button>
{#if saved_font_sizes || has_any_size_override}
<button
type="button"
class="btn btn-xs preset-tonal-warning shrink-0"
disabled={lock_sizes_status === 'saving'}
onclick={reset_font_sizes_to_auto}
title="Reset all font sizes to auto and clear saved sizes from badge record">
↺ Auto
</button>
{/if}
</div>
{#if saved_font_sizes}
<p class="px-1 text-[9px] text-green-600 dark:text-green-400">
Sizes locked on this badge
{#if saved_font_sizes.name}· Name {saved_font_sizes.name}px{/if}
{#if saved_font_sizes.title}· Title {saved_font_sizes.title}px{/if}
</p>
{/if}
</div>
<!-- Print Position Offset
Per-browser calibration for physical printer alignment.
Stored in localStorage by the parent page — each workstation keeps

View File

@@ -100,11 +100,41 @@ function send_review_email() {
// Controls live in Comp_badge_print_controls (right panel) via $bindable().
// Constants and adjust logic are defined there; only the state lives here so
// the values can be forwarded to both the controls and the badge render.
//
// Initialization order: badge cfg_json.font_sizes takes precedence (persisted per-badge),
// falling back to null (auto) on first load. The controls panel's "Lock Sizes" button
// saves current sizes back to cfg_json so they survive page reloads and cross-kiosk.
let font_size_name: number | null = $state(null);
let font_size_title: number | null = $state(null);
let font_size_affiliations: number | null = $state(null);
let font_size_location: number | null = $state(null);
// Track whether we've applied the saved sizes for this badge yet.
// Prevents re-applying on every liveQuery tick after the user adjusts sizes.
let _font_sizes_loaded_for: string | null = null;
$effect(() => {
const badge = $lq__event_badge_obj;
if (!badge?.event_badge_id) return;
// Only apply once per badge ID — don't clobber user adjustments on background refreshes
if (_font_sizes_loaded_for === badge.event_badge_id) return;
_font_sizes_loaded_for = badge.event_badge_id;
try {
const cfg = typeof badge.cfg_json === 'string'
? JSON.parse(badge.cfg_json)
: (badge.cfg_json ?? {});
const fs = cfg?.font_sizes;
if (fs) {
font_size_name = fs.name ?? null;
font_size_title = fs.title ?? null;
font_size_affiliations = fs.affiliations ?? null;
font_size_location = fs.location ?? null;
}
} catch {
// Malformed cfg_json — stay with null (auto) for all fields
}
});
// Per-browser print workstation tweaks.
// Each workstation/printer is calibrated independently — values are stored in
// localStorage so they survive page reloads and browser restarts without any