From c7063806b7349c4a58a35086e3bb79c427ecc601 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 12 Mar 2026 13:48:08 -0400 Subject: [PATCH] refactor: v2 badge render display-only + print button to controls panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ae_comp__badge_obj_view_v2.svelte: removed all inline edit-mode logic (floating Edit/Save/Cancel panel, placeholder list, input fields, save/cancel functions). V2 is now purely a display component — editing happens in the right panel (ae_comp__badge_print_controls.svelte) via liveQuery reactivity. display_* values are now $derived directly from lq__event_badge_obj. Fixes effective_badge_type_code CSS class (was always empty in previous v2). - ae_comp__badge_print_controls.svelte: added "Print Badge" button at the top of the panel. Increments print_count, fires window.print(), then navigates to badge search. This is now the canonical print action for v2; the header "Print Now" button is a shortcut that calls window.print() only (no count tracking). Clarified $ae_loc.edit_mode comment — global AE Edit Mode is not a badge-specific edit toggle; used here only to gate reprints. - print/+page.svelte: added clarifying comments on is_edit_mode (global vs local) and the header "Print Now" button (shortcut only, no count tracking). Co-Authored-By: Claude Sonnet 4.6 --- .../ae_comp__badge_obj_view_v2.svelte | 856 ++++-------------- .../ae_comp__badge_print_controls.svelte | 94 +- .../badges/[badge_id]/print/+page.svelte | 11 +- 3 files changed, 270 insertions(+), 691 deletions(-) diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte index 1605f391..bdb28af1 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte @@ -2,23 +2,19 @@ /** * ae_comp__badge_obj_view_v2.svelte * - * V2 badge render component — replaces the manual longest_str_part() heuristic - * with element_fit_text.svelte (binary-search auto-scaling) for all four text - * fields (name, title, affiliations, location). + * V2 badge render component — display-only. No inline edit mode. * - * Auto-scaling is suppressed in edit mode (inputs need fixed size, not auto-scale). - * Manual override props (font_size_*) from print controls still work — they are - * forwarded to Element_fit_text as manual_size, which disables auto-scale for - * that field and sets the size directly. + * Editing is handled entirely by the right-panel controls component + * (ae_comp__badge_print_controls.svelte). Changes saved there flow + * back via liveQuery (IDB → reactive UI update) automatically. * * Differences from v1 (ae_comp__badge_obj_view.svelte): - * - Imports Element_fit_text - * - Removed: full_name_class_size, professional_title_class_size, - * affiliations_class_size, location_class_size state vars - * - Removed: longest_str_part() function - * - Removed: the $effect block that computed those size classes - * - Text fields in display mode are wrapped with - * - Edit mode uses plain divs (no auto-scaling on inputs) + * - No inline edit mode — floating Edit/Save/Cancel panel removed + * - No handle_save_changes / handle_cancel_changes / handle_print_badge + * - Print button lives in ae_comp__badge_print_controls.svelte + * - Uses Element_fit_text (binary-search auto-scaling) for all text fields + * - display_* values are $derived directly from lq__event_badge_obj + * - Removed: longest_str_part() heuristic sizing */ interface Props { @@ -26,37 +22,29 @@ event_badge_id: string; lq__event_badge_obj?: any; lq__event_badge_template_obj?: any; - update_status?: string; - update_complete?: boolean; + /** Kept for API compatibility; unused in v2 (no inline edit mode). */ is_review_mode?: boolean; - /** Optional px override for name font size. When undefined/null, auto-scaling runs. */ + /** Optional px override for name font size. null/undefined → auto-scaling runs. */ font_size_name?: number | null; - /** Optional px override for professional title font size. When undefined/null, auto-scaling runs. */ + /** Optional px override for professional title font size. */ font_size_title?: number | null; - /** Optional px override for affiliations font size. When undefined/null, auto-scaling runs. */ + /** Optional px override for affiliations font size. */ font_size_affiliations?: number | null; - /** Optional px override for location font size. When undefined/null, auto-scaling runs. */ + /** Optional px override for location font size. */ font_size_location?: number | null; log_lvl?: number; } + // event_id, is_review_mode, log_lvl are in Props for API compat (callers may pass them) + // but are not needed in this display-only component — omit from destructuring. let { - event_id, event_badge_id, lq__event_badge_obj, lq__event_badge_template_obj, - update_status = $bindable('idle'), - update_complete = $bindable(true), - is_review_mode = false, - // V2: no numeric defaults — undefined → auto-scale runs via Element_fit_text. - // When the user adjusts a field in the print controls panel, a number is pushed - // here via the $bindable binding, which disables auto-scale for that field. - // Reset (×) in the print controls panel restores null → auto-scale resumes. font_size_name, font_size_title, font_size_affiliations, font_size_location, - log_lvl = 0 }: Props = $props(); // Badge layout CSS — compiled in, hot-reloads in dev. @@ -65,31 +53,15 @@ import '$lib/ae_events/badges/css/badge_layout_epson_4x5_fanfold.css'; import '$lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css'; - // *** Import Svelte specific import { browser } from '$app/environment'; - import { goto } from '$app/navigation'; - - import type { key_val } from '$lib/stores/ae_stores'; import { core_func } from '$lib/ae_core/ae_core_functions'; - import { - ae_snip, - ae_loc, - ae_sess, - ae_api, - ae_trig, - slct, - slct_trigger - } from '$lib/stores/ae_stores'; - import { events_func } from '$lib/ae_events_functions'; + import { ae_loc, slct } from '$lib/stores/ae_stores'; // V2: auto-scaling text component import Element_fit_text from '$lib/elements/element_fit_text.svelte'; - // *** Variables - let ae_triggers: key_val = $state({}); - - // Badge type list is derived from the template — each event/template defines its own set. - // Falls back to empty array if template not loaded or badge_type_list is invalid JSON. + // --- Badge type list from template --- + // Each item: { code: string, name: string }. Drives footer display + (in controls) dropdown. let badge_type_code_li = $derived.by(() => { const raw = $lq__event_badge_template_obj?.badge_type_list; if (!raw) return []; @@ -101,33 +73,50 @@ } }); - // Human-readable name for the current badge type, printed on the badge footer. - // Priority: badge_type_override (staff custom name) → badge_type (base import name) - // → template badge_type_list lookup by effective code → code itself as last resort. + // --- Effective display values (override ?? base) --- + // $derived keeps these reactive to liveQuery updates from the right panel saves. + let display_name = $derived( + $lq__event_badge_obj?.full_name_override ?? $lq__event_badge_obj?.full_name ?? '' + ); + let display_title = $derived( + $lq__event_badge_obj?.professional_title_override ?? $lq__event_badge_obj?.professional_title ?? '' + ); + let display_affiliations = $derived( + $lq__event_badge_obj?.affiliations_override ?? $lq__event_badge_obj?.affiliations ?? '' + ); + let display_location = $derived( + $lq__event_badge_obj?.location_override ?? $lq__event_badge_obj?.location ?? '' + ); + + // Effective badge type code — CSS class hook for per-event stylesheets. + // Priority: badge_type_code_override → badge_type_code + let effective_badge_type_code = $derived( + $lq__event_badge_obj?.badge_type_code_override ?? $lq__event_badge_obj?.badge_type_code ?? '' + ); + + // Human-readable badge type name — printed on the badge footer. + // Priority: badge_type_override (staff custom name) → badge_type (import name) + // → template list lookup by effective code → code itself as last resort. // - // Why separate from code: the code is the CSS class hook for per-event stylesheets. - // A special-case attendee can share a code (and thus styling) with "Member" but have a + // A special-case attendee can share a code (and CSS styling) with "Member" but have a // custom displayed name like "Life Member" stored in badge_type_override. let badge_type_name = $derived.by(() => { - // Staff override name takes priority const override_name = $lq__event_badge_obj?.badge_type_override; if (override_name) return override_name; - // Base name from import/registration system const base_name = $lq__event_badge_obj?.badge_type; if (base_name) return base_name; - // Fall back to template list lookup using the effective code const found = (badge_type_code_li as { code: string; name: string }[]) - .find(item => item.code === editable_badge_type_code); - return found?.name ?? editable_badge_type_code ?? ''; + .find(item => item.code === effective_badge_type_code); + return found?.name ?? effective_badge_type_code ?? ''; }); - // Show the badge back section when duplex=1 (or when duplex is not yet set, as a safe default). - // duplex=0/false → single-sided print (e.g. Zebra ZC10L PVC cards); back section is hidden. + // Show badge back when duplex=1 (or not yet set — safe default for existing templates). + // duplex=0/false → single-sided (e.g. Zebra ZC10L PVC cards); back section is hidden. let show_badge_back = $derived( $lq__event_badge_template_obj?.duplex == null || !!$lq__event_badge_template_obj?.duplex ); - // Receipt and ticket sections are currently disabled pending redesign. + // Receipt and ticket sections — disabled pending redesign. let show_receipt = $derived(false); let show_tickets = $derived(false); @@ -140,18 +129,13 @@ * name — height of the name Element_fit_text * title — height of the title Element_fit_text * grp_aff_loc — height of the affiliations_location container - * grp_aff_loc_flex — how affiliations vs location are distributed within that container + * grp_aff_loc_flex — how affiliations vs location are distributed * affiliations — height of the affiliations Element_fit_text * location — height of the location Element_fit_text * * Flex values: 'around' | 'between' | 'even' | 'center' | 'start' | 'end' * (mapped to space-around, space-between, space-evenly, center, flex-start, flex-end) * - * Layout codes (from ae_events/badges/css/): - * badge_3.5x5.5_pvc — Zebra ZC10L PVC card, 3.5" × 5.5", single-sided - * badge_4x5_fanfold — Epson 4" × 5" fanfold, duplex - * badge_4x6_fanfold — Default 4" × 6" fanfold, duplex - * * TODO: Move to badge template config (cfg_json) once multi-layout events are needed. * Tune these values with real badge data and a ruler. */ @@ -212,84 +196,40 @@ return map[val] ?? 'space-around'; } - // *** Set initial variables + // Sync selected badge ID to the slct store (used by other app modules for context). $effect(() => { $slct.event_badge_id = event_badge_id; }); - let trigger = $state(null); - let initial_loading_promise = $state(null); + // QR code + let hide_qr: null | boolean = $state(null); + let qr_data_url: any = $state(''); + let qr_error_message = $state(''); - let event_badge_obj_load_promise = $state(null); - let event_badge_obj_update_promise = $state(null); - - let event_badge_qr_mecard_get_promise = $state(null); - let event_badge_qr_id_get_promise = $state(null); - - let show_event_badge_tools_modal: boolean = $state(false); - let show_restricted_fields: boolean = $state(false); - - // Editable fields - let editable_full_name_override: string | null = $state(null); - let editable_professional_title_override: string | null = $state(null); - let editable_affiliations_override: string | null = $state(null); - let editable_location_override: string | null = $state(null); - let editable_allow_tracking: boolean | null = $state(null); - let editable_email: string | null = $state(null); - let editable_badge_type_code: string | null = $state(null); - - // Manage edit state locally - let edit_mode_active: boolean = $state(false); - - // Initialize editable fields when lq__event_badge_obj changes $effect(() => { - if ($lq__event_badge_obj) { - if (log_lvl) { - console.log('Initializing editable fields from lq__event_badge_obj'); - } - editable_full_name_override = - $lq__event_badge_obj.full_name_override ?? - $lq__event_badge_obj.full_name; - editable_professional_title_override = - $lq__event_badge_obj.professional_title_override ?? - $lq__event_badge_obj.professional_title; - editable_affiliations_override = - $lq__event_badge_obj.affiliations_override ?? - $lq__event_badge_obj.affiliations; - editable_location_override = - $lq__event_badge_obj.location_override ?? - $lq__event_badge_obj.location; - editable_allow_tracking = - $lq__event_badge_obj.allow_tracking ?? null; - editable_email = $lq__event_badge_obj.email ?? null; - // Use staff override code if set; base code from import/registration otherwise. - editable_badge_type_code = - $lq__event_badge_obj.badge_type_code_override ?? - $lq__event_badge_obj.badge_type_code ?? null; - - // Only set the local edit state — never touch $ae_loc.edit_mode here. - // $ae_loc.edit_mode is a user preference; this component must not override it. - if (is_review_mode) { - edit_mode_active = true; - } else { - edit_mode_active = false; + if (browser && $lq__event_badge_obj?.event_badge_id) { + qr_error_message = ''; + qr_data_url = ''; + const params: any = { + obj_type: 'event_badge', + obj_id: $lq__event_badge_obj.event_badge_id + }; + try { + qr_data_url = core_func.js_generate_qr_code('obj', params); + } catch (error: any) { + qr_error_message = error?.message || 'Unknown error'; + console.error(error); } } }); - let show_print_msg: null | boolean = $state(null); - let hide_qr: null | boolean = $state(null); - - let use_badge_type_code = $state(''); + /* *** BEGIN *** Legacy ticket/option state — move to template config in future */ let option_ticket_1_override = $state(''); let option_ticket_2_override = $state(''); let option_ticket_3_override = $state(''); let option_other_1_override = $state(''); let option_other_2_override = $state(''); - let slct_badge_type = ''; - - /* *** BEGIN *** This should be moved out */ let option_ticket_1_display_opt = 'front_bool'; let option_ticket_2_display_opt = 'front_bool'; let option_ticket_3_display_opt = 'front_bool'; @@ -314,227 +254,16 @@ code_to_html.option_2['true'] = ''; code_to_html.option_2['True'] = ''; code_to_html.option_2['First Time '] = ''; - /* *** END *** This should be moved out */ - - let qr_data_url: any = $state(''); - let qr_error_message = $state(''); - - // Trigger doing a update for event badge - $effect(() => { - if (ae_triggers.event_badge_update) { - ae_triggers.event_badge_update = false; - update_status = 'loading'; - update_complete = false; - } - }); - - $effect(() => { - if (browser && $lq__event_badge_obj?.event_badge_id) { - qr_error_message = ''; - qr_data_url = ''; - - let params: any = {}; - params.obj_type = 'event_badge'; - params.obj_id = $lq__event_badge_obj?.event_badge_id; - - try { - qr_data_url = core_func.js_generate_qr_code('obj', params); - } catch (error: any) { - qr_error_message = error?.message || 'Unknown error'; - console.error(error); - } - } - }); - - // *** Functions and Logic - function prevent_default(fn: (event: T) => void) { - return function (event: T) { - event.preventDefault(); - fn(event); - }; - } - - async function handle_save_changes() { - if (!$lq__event_badge_obj?.event_badge_id) { - console.error('Cannot save changes: event_badge_id is missing.'); - return; - } - - update_status = 'loading'; - update_complete = false; - - const data_to_update: key_val = {}; - - // Only include fields that have actually changed - if ( - editable_full_name_override !== - ($lq__event_badge_obj.full_name_override ?? - $lq__event_badge_obj.full_name) - ) { - data_to_update.full_name_override = editable_full_name_override; - } - if ( - editable_professional_title_override !== - ($lq__event_badge_obj.professional_title_override ?? - $lq__event_badge_obj.professional_title) - ) { - data_to_update.professional_title_override = - editable_professional_title_override; - } - if ( - editable_affiliations_override !== - ($lq__event_badge_obj.affiliations_override ?? - $lq__event_badge_obj.affiliations) - ) { - data_to_update.affiliations_override = - editable_affiliations_override; - } - if ( - editable_location_override !== - ($lq__event_badge_obj.location_override ?? - $lq__event_badge_obj.location) - ) { - data_to_update.location_override = editable_location_override; - } - if (editable_allow_tracking !== $lq__event_badge_obj.allow_tracking) { - data_to_update.allow_tracking = editable_allow_tracking; - } - if (editable_email !== $lq__event_badge_obj.email) { - data_to_update.email = editable_email; - } - // Compare against the effective code (override ?? base) to detect a real change. - // Staff edits go to badge_type_code_override — the base import code is never overwritten. - const stored_effective_code = - $lq__event_badge_obj.badge_type_code_override ?? - $lq__event_badge_obj.badge_type_code; - if (editable_badge_type_code !== stored_effective_code) { - data_to_update.badge_type_code_override = editable_badge_type_code; - // Keep badge_type_override in sync — look up the name from the template list. - // Edge case: if a badge needs a custom name that differs from the template list - // (e.g. "Life Member" for a "member" code), set badge_type_override manually - // in the DB. Do not use the dropdown — it will overwrite the custom name. - data_to_update.badge_type_override = - (badge_type_code_li as { code: string; name: string }[]) - .find(item => item.code === editable_badge_type_code)?.name - ?? editable_badge_type_code; - } - - if (Object.keys(data_to_update).length === 0) { - console.log('No changes to save.'); - update_status = 'done'; - update_complete = true; - if (!is_review_mode) { - edit_mode_active = false; - } - return; - } - - try { - await events_func.update_ae_obj__event_badge({ - api_cfg: $ae_api, - event_id: event_id, - event_badge_id: $lq__event_badge_obj.event_badge_id, - data_kv: data_to_update, - log_lvl: log_lvl - }); - update_status = 'done'; - update_complete = true; - if (!is_review_mode) { - edit_mode_active = false; - } - } catch (error) { - console.error('Error saving changes:', error); - update_status = 'error'; - update_complete = true; - } - } - - function handle_cancel_changes() { - if ($lq__event_badge_obj) { - editable_full_name_override = - $lq__event_badge_obj.full_name_override ?? - $lq__event_badge_obj.full_name; - editable_professional_title_override = - $lq__event_badge_obj.professional_title_override ?? - $lq__event_badge_obj.professional_title; - editable_affiliations_override = - $lq__event_badge_obj.affiliations_override ?? - $lq__event_badge_obj.affiliations; - editable_location_override = - $lq__event_badge_obj.location_override ?? - $lq__event_badge_obj.location; - editable_allow_tracking = - $lq__event_badge_obj.allow_tracking ?? null; - editable_email = $lq__event_badge_obj.email ?? null; - editable_badge_type_code = - $lq__event_badge_obj.badge_type_code_override ?? - $lq__event_badge_obj.badge_type_code ?? null; - } - if (!is_review_mode) { - edit_mode_active = false; - } - update_status = 'idle'; - update_complete = true; - } - - let print_status = $state('idle'); // 'idle' | 'loading' | 'done' | 'error' - - async function handle_print_badge() { - if (!$lq__event_badge_obj?.event_badge_id) { - console.error('Cannot print badge: event_badge_id is missing.'); - return; - } - - print_status = 'loading'; - - const now = new Date().toISOString(); - const current_print_count = $lq__event_badge_obj.print_count ?? 0; - const is_first_print = current_print_count === 0; - - const data_to_update: key_val = { - print_count: current_print_count + 1, - print_last_datetime: now - }; - - if (is_first_print) { - data_to_update.print_first_datetime = now; - } - - try { - await events_func.update_ae_obj__event_badge({ - api_cfg: $ae_api, - event_id: event_id, - event_badge_id: $lq__event_badge_obj.event_badge_id, - data_kv: data_to_update, - log_lvl: log_lvl - }); - print_status = 'done'; - console.log(`Badge printed. Count: ${data_to_update.print_count}`); - - // Trigger browser print dialog (records the print count first so it's always logged) - if (browser) window.print(); - - // Brief success flash, then return to badge search - setTimeout(() => { - goto(`/events/${event_id}/badges`); - }, 1000); - } catch (error) { - console.error('Error printing badge:', error); - print_status = 'error'; - setTimeout(() => { - print_status = 'idle'; - }, 3000); - } - } + /* *** END *** Legacy ticket/option state */ - +
{$lq__event_badge_template_obj?.name ?? '—'} | {$lq__event_badge_template_obj?.layout ?? '(no layout)'} | - v2 + v2
-
- {#if edit_mode_active} - - - {:else} - - {/if} - - - - -
-

- Show list of fields that they can edit here. This may - need to broken down in to sections that can be - collapsed. -

-
    -
  • Full Name
  • -
  • Professional Title
  • -
  • Affiliations
  • -
  • Location
  • -
  • Option Ticket 1
  • -
  • Option Ticket 2
  • -
  • Option Ticket 3
  • -
  • Option Other 1
  • -
  • Option Other 2
  • -
  • Badge Type
  • -
  • Allow Tracking
  • -
  • Show Print Message
  • -
  • Hide QR Code
  • -
-
-
- {#if $lq__event_badge_template_obj.header_path}
+
- {#if edit_mode_active} -
- -
- {:else} - - - {#if editable_full_name_override} - {@html editable_full_name_override.trim()} - {:else} - -- no name -- - {/if} - - - {/if} + + + {#if display_name} + {@html display_name.trim()} + {:else} + -- no name -- + {/if} + + - {#if editable_professional_title_override || edit_mode_active} + {#if display_title} - {#if edit_mode_active} -
- -
- {:else} - - {@html editable_professional_title_override} - - {/if} + + {@html display_title} + {/if}
- {#if editable_affiliations_override || editable_location_override || edit_mode_active || editable_email || edit_mode_active || editable_allow_tracking !== null || edit_mode_active} + {#if display_affiliations || display_location} +
- {#if editable_affiliations_override || edit_mode_active} + {#if display_affiliations} - {#if edit_mode_active} -
- -
- {:else} - - {@html editable_affiliations_override} - - {/if} + + {@html display_affiliations} + {/if} - {#if editable_location_override || edit_mode_active} + {#if display_location} - {#if edit_mode_active} -
- -
- {:else} - + {@html display_location} - {@html editable_location_override} - - {/if} - {/if} - - {#if edit_mode_active} - - - {/if} - - {#if edit_mode_active} - -
- -
+ {/if}
{/if} - {#if ['front_bool', 'front_back_bool'].includes(option_ticket_1_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_2_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_3_display_opt) || $lq__event_badge_template_obj?.show_qr_front || edit_mode_active} + {#if ['front_bool', 'front_back_bool'].includes(option_ticket_1_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_2_display_opt) || ['front_bool', 'front_back_bool'].includes(option_ticket_3_display_opt) || $lq__event_badge_template_obj?.show_qr_front}
@@ -972,7 +496,7 @@ @@ -1238,36 +746,6 @@
{/if} - {#if $lq__event_badge_template_obj.exhibitor_info} -
- Exhibitor Info: -
- {/if} - - {#if $lq__event_badge_template_obj.presenter_info} -
- Presenter Info: -
- {/if} - - {#if $lq__event_badge_template_obj.staff_info} -
- Staff Info: -
- {/if} - - {#if $lq__event_badge_template_obj.vip_info} -
- VIP Info: -
- {/if} - - {#if $lq__event_badge_template_obj.vote_info} -
-
Voting Info:
-
- {/if} - {#if $lq__event_badge_template_obj.show_qr_back}