diff --git a/src/routes/events/[event_id]/(badges)/badges/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/+page.svelte index a0fb095b..d2beb3a2 100644 --- a/src/routes/events/[event_id]/(badges)/badges/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/+page.svelte @@ -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 diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte index bf6f6b44..cb31ad11 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_obj_li.svelte @@ -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 = $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 = $state({}); +let print_count_status: Record = $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( {/if} + + {#if is_trusted && is_edit_mode} + + {/if} + {#if is_trusted && is_edit_mode} PC: - {print_count} + {#if is_admin} + 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'} + + {/if} + {:else} + {print_count} + {/if} FP: diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte index 2359d9c5..aa0f59bf 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte @@ -262,6 +262,15 @@ function handle_qr_scan_result(event: { {#if $ae_loc.edit_mode} +