feat(badges): hide toggle, print count editor, show hidden filter

- Hide/Unhide toggle button (Trusted + Edit Mode) on each badge row in the list; badge disappears immediately when hidden unless Show Hidden is active
- Print count inline editor in debug row (Admin + Edit Mode); updates count only, no timestamp changes
- "Show Hidden" checkbox in search filters (Trusted + Edit Mode); wires through IDB fast-path, API hidden param, and visible_badge_obj_li filter
- show_hidden requires edit_mode to be active — reverts to hiding hidden badges when edit mode is off

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-13 22:21:08 -04:00
parent 6aeaef6f1d
commit 7bc7bf5554
3 changed files with 109 additions and 7 deletions

View File

@@ -168,6 +168,7 @@ let search_params = $derived({
printed: badges_loc.current.qry_printed_status,
aff: (badges_loc.current.qry_affiliations ?? '').toLowerCase().trim(),
sort: badges_loc.current.qry_sort_order,
show_hidden: badges_loc.current.show_hidden,
event_id: $events_slct?.event_id,
remote_first: badges_loc.current.qry__remote_first,
result_limit: effective_search_limits.result_limit,
@@ -217,11 +218,13 @@ async function handle_search_refresh(params: any) {
const result_limit = params.result_limit;
const min_chars = params.min_chars;
const show_hidden = params.show_hidden;
// Defense-in-depth: enforce min_chars even if the search component lets one through.
// Exception: if the user has set a non-default filter or sort, that is explicit intent —
// run the search even without a text query.
const has_active_filters =
printed_status !== 'all' || !!type_code || !!aff_str || !!params.sort;
printed_status !== 'all' || !!type_code || !!aff_str || !!params.sort || show_hidden;
if (qry_str.length < min_chars && !has_active_filters) {
untrack(() => {
event_badge_id_li = [];
@@ -239,6 +242,9 @@ async function handle_search_refresh(params: any) {
.where('event_id')
.equals(event_id)
.filter((badge) => {
// Exclude hidden badges unless show_hidden is active
if (!show_hidden && badge.hide) return false;
if (type_code && badge.badge_type_code !== type_code)
return false;
@@ -416,6 +422,7 @@ async function handle_search_refresh(params: any) {
type_code: type_code || null,
printed_status: printed_status,
affiliations_qry_str: aff_str || null,
hidden: show_hidden ? 'all' : 'not_hidden',
order_by_li: order_by_li,
limit: result_limit,
log_lvl: 0

View File

@@ -19,10 +19,11 @@ let {
hide_badge_type = false
}: Props = $props();
import { ae_loc } from '$lib/stores/ae_stores';
import { ae_loc, ae_api } 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,
@@ -41,9 +42,55 @@ 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
@@ -96,18 +143,23 @@ let visible_badge_obj_li = $derived(
if (list === undefined || list === null) return null;
if (!Array.isArray(list)) return [];
// show_hidden requires trusted + edit_mode — it's an admin override, not a
// persistent search filter. Turning off edit mode reverts to hiding hidden badges.
const show_hidden_badges = badges_loc.current.show_hidden && is_trusted && is_edit_mode;
const filtered = list.filter((item: any) => {
if (!item) return false;
if (is_trusted) {
// Trusted staff and above: qry_printed_status is the authoritative control.
// Filter state persists across edit mode toggles — intentional. Staff set
// their filter and it stays regardless of whether edit mode is on or off.
const hide_ok = show_hidden_badges || !item.hide;
const ps = badges_loc.current.qry_printed_status;
if (ps === 'printed') return (item.print_count ?? 0) >= 1 && !item.hide;
if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && !item.hide;
return !item.hide; // 'all' — show everything non-hidden
if (ps === 'printed') return (item.print_count ?? 0) >= 1 && hide_ok;
if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && hide_ok;
return hide_ok; // 'all'
}
// Public (kiosk) / authenticated / anonymous: only unprinted.
// Public (kiosk) / authenticated / anonymous: only unprinted, never hidden.
// Badge kiosks run at public_access — attendees should only see their own
// unprinted badge, never a list of already-printed ones.
return (item.print_count ?? 0) < 1 && !item.hide;
@@ -268,6 +320,25 @@ 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
@@ -346,7 +417,22 @@ let visible_badge_obj_li = $derived(
</span>
<span class="flex items-center gap-1">
<span class="font-bold opacity-50">PC:</span>
{print_count}
{#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}
</span>
<span class="flex items-center gap-1">
<span class="font-bold opacity-50">FP:</span>

View File

@@ -262,6 +262,15 @@ function handle_qr_scan_result(event: {
</button>
{#if $ae_loc.edit_mode}
<label
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Show Hidden </span>
<input
type="checkbox"
bind:checked={badges_loc.current.show_hidden}
onchange={handle_search_trigger}
class="checkbox checkbox-sm" />
</label>
<label
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Remote First </span>