refactor(badges): move hide toggle + print count editor to individual badge view

Hide/Unhide and print count edit belong on the per-badge page (print controls
staff section), not the search list — the list was getting too crowded.

- ae_comp__badge_obj_li: removed hide toggle, print count input, and the
  ae_api/events_func imports that were only there to support them
- ae_comp__badge_print_controls: added Hide Badge button (Trusted, top of staff
  section) and Print Count editor (Admin+, below hide); both reuse the existing
  save_field/field_save_status pattern for consistent spinner/done/error feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-13 22:44:58 -04:00
parent bf31f13650
commit 5203104fef
2 changed files with 69 additions and 83 deletions

View File

@@ -112,6 +112,7 @@ let is_auth = $derived($ae_loc.authenticated_access === true);
// This component NEVER writes to $ae_loc.edit_mode. Read-only usage only:
// — 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);
let is_admin = $derived($ae_loc.administrator_access === true);
// --- Per-template controls config ---
// Stored in event_badge_template.other_json as { controls_cfg: { shown?, auth_editable? } }.
@@ -1554,6 +1555,72 @@ let allow_tracking_open = $derived(
<div class="ctrl-accordion" class:open={staff_open}>
<div class="ctrl-accordion-inner space-y-1 pt-0.5">
<!-- === HIDE BADGE ===
Removes this badge from search results for all users.
Use for duplicates, test records, or badges that should not be printed.
Trusted + Edit Mode. Visible even when badge is already hidden so staff
can recover it without needing to enable Show Hidden in search first. -->
<div class="flex items-center gap-2 px-2 py-1">
<button
type="button"
class="btn btn-sm w-full justify-between"
class:preset-tonal-warning={$lq__event_badge_obj?.hide}
class:preset-tonal-surface={!$lq__event_badge_obj?.hide}
disabled={field_save_status['hide'] === 'saving'}
onclick={() => save_field('hide', { hide: !$lq__event_badge_obj?.hide })}
title={$lq__event_badge_obj?.hide
? 'Badge is hidden — click to unhide and restore to search results'
: 'Hide this badge from search results'}>
<span class="flex items-center gap-1.5">
{#if field_save_status['hide'] === 'saving'}
<LoaderCircle size="13" class="animate-spin shrink-0" />
{$lq__event_badge_obj?.hide ? 'Unhiding…' : 'Hiding…'}
{:else if $lq__event_badge_obj?.hide}
<Eye size="13" class="shrink-0" />
Unhide Badge
{:else}
<EyeOff size="13" class="shrink-0" />
Hide Badge
{/if}
</span>
{#if field_save_status['hide'] === 'done'}
<Check size="13" class="text-success-500 shrink-0" />
{/if}
</button>
</div>
<!-- === PRINT COUNT (Admin+) ===
Correction tool for when the physical count diverges from the DB
(e.g. badge was printed offline or kiosk count reset). Does NOT
change print_first_datetime / print_last_datetime — timestamps
are only written by the actual Print Badge action. -->
{#if is_admin}
<div class="px-2 py-1">
<p class="field-label mb-1">
Print Count
<span class="ml-1 text-[9px] font-normal text-gray-400 normal-case tracking-normal">Admin · timestamps unchanged</span>
</p>
<div class="flex items-center gap-2">
<input
type="number"
min="0"
value={print_count}
onblur={(e) => {
const v = Math.max(0, Math.round(Number((e.currentTarget as HTMLInputElement).value)));
if (v !== print_count) save_field('print_count', { print_count: v });
}}
onkeydown={(e) => { if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur(); }}
disabled={field_save_status['print_count'] === 'saving'}
class="input input-sm w-20 text-center font-mono" />
{#if field_save_status['print_count'] === 'saving'}
<LoaderCircle size="13" class="animate-spin opacity-70" />
{:else if field_save_status['print_count'] === 'done'}
<Check size="13" class="text-success-500" />
{/if}
</div>
</div>
{/if}
<!-- Chrome visibility toggle: hides page header + sys bar for a clean workspace.
Keyboard shortcut [H] does the same thing from anywhere on the page. -->
<div class="flex items-center gap-2 px-2 py-1">

View File

@@ -19,11 +19,10 @@ let {
hide_badge_type = false
}: Props = $props();
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
Check,
Eye,
@@ -42,55 +41,9 @@ let copy_status: Record<string, 'idle' | 'copied'> = $state({});
// Access level shortcuts
let is_trusted = $derived($ae_loc.trusted_access === true);
let is_admin = $derived($ae_loc.administrator_access === true);
let is_public = $derived($ae_loc.public_access === true); // public passcode or higher — may print first prints
let is_edit_mode = $derived($ae_loc.edit_mode === true);
// Per-badge async action states
let hide_status: Record<string, 'idle' | 'loading'> = $state({});
let print_count_status: Record<string, 'idle' | 'saving'> = $state({});
async function toggle_badge_hide(badge_obj: any) {
const id = badge_obj.event_badge_id;
if (!id || hide_status[id] === 'loading') return;
hide_status[id] = 'loading';
try {
await events_func.update_ae_obj__event_badge({
api_cfg: $ae_api,
event_id: badge_obj.event_id,
event_badge_id: id,
data_kv: { hide: !badge_obj.hide },
log_lvl
});
} catch (e) {
console.error('Failed to toggle hide:', e);
}
hide_status[id] = 'idle';
}
// Admin-only: edit the raw print count. Does NOT touch timestamps — those are
// only set by the actual print action. This is a correction tool (e.g. "badge
// was printed offline, count needs to match reality").
async function save_print_count(badge_obj: any, new_count: number) {
const id = badge_obj.event_badge_id;
if (!id || print_count_status[id] === 'saving') return;
const count = Math.max(0, Math.round(new_count));
if (count === (badge_obj.print_count ?? 0)) return; // no-op
print_count_status[id] = 'saving';
try {
await events_func.update_ae_obj__event_badge({
api_cfg: $ae_api,
event_id: badge_obj.event_id,
event_badge_id: id,
data_kv: { print_count: count },
log_lvl
});
} catch (e) {
console.error('Failed to update print count:', e);
}
print_count_status[id] = 'idle';
}
/**
* Obscures an email address for display to non-trusted users.
* e.g. john.doe@example.com → joh***@example.com
@@ -320,25 +273,6 @@ let visible_badge_obj_li = $derived(
</a>
{/if}
<!-- 1.5. Hide/Unhide badge: Trusted + Edit Mode -->
{#if is_trusted && is_edit_mode}
<button
type="button"
onclick={() => toggle_badge_hide(event_badge_obj)}
disabled={hide_status[event_badge_obj.event_badge_id] === 'loading'}
class="btn btn-sm flex items-center gap-1 border {event_badge_obj.hide ? 'preset-tonal-warning border-warning-200-800' : 'preset-tonal-surface border-surface-300-700'}"
title={event_badge_obj.hide ? 'Unhide badge' : 'Hide badge'}>
{#if hide_status[event_badge_obj.event_badge_id] === 'loading'}
<LoaderCircle size="1em" class="animate-spin" />
{:else if event_badge_obj.hide}
<Eye size="1em" />
{:else}
<EyeOff size="1em" />
{/if}
<span class="hidden sm:inline">{event_badge_obj.hide ? 'Unhide' : 'Hide'}</span>
</button>
{/if}
<!-- 2. Direct Review link: Trusted + Edit Mode (navigates to /review) -->
{#if is_trusted && is_edit_mode}
<a
@@ -417,22 +351,7 @@ let visible_badge_obj_li = $derived(
</span>
<span class="flex items-center gap-1">
<span class="font-bold opacity-50">PC:</span>
{#if is_admin}
<input
type="number"
min="0"
value={print_count}
onblur={(e) => save_print_count(event_badge_obj, Number((e.currentTarget as HTMLInputElement).value))}
onkeydown={(e) => { if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur(); }}
disabled={print_count_status[event_badge_obj.event_badge_id] === 'saving'}
class="w-14 rounded border border-surface-300 bg-surface-100 px-1 text-center font-mono text-[10px] dark:border-surface-700 dark:bg-surface-800"
title="Edit print count (Admin+). Does not change timestamps." />
{#if print_count_status[event_badge_obj.event_badge_id] === 'saving'}
<LoaderCircle size="0.8em" class="animate-spin opacity-70" />
{/if}
{:else}
{print_count}
{/if}
{print_count}
</span>
<span class="flex items-center gap-1">
<span class="font-bold opacity-50">FP:</span>