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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user