refactor: v2 badge render display-only + print button to controls panel
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -2,23 +2,19 @@
|
|||||||
/**
|
/**
|
||||||
* ae_comp__badge_obj_view_v2.svelte
|
* ae_comp__badge_obj_view_v2.svelte
|
||||||
*
|
*
|
||||||
* V2 badge render component — replaces the manual longest_str_part() heuristic
|
* V2 badge render component — display-only. No inline edit mode.
|
||||||
* with element_fit_text.svelte (binary-search auto-scaling) for all four text
|
|
||||||
* fields (name, title, affiliations, location).
|
|
||||||
*
|
*
|
||||||
* Auto-scaling is suppressed in edit mode (inputs need fixed size, not auto-scale).
|
* Editing is handled entirely by the right-panel controls component
|
||||||
* Manual override props (font_size_*) from print controls still work — they are
|
* (ae_comp__badge_print_controls.svelte). Changes saved there flow
|
||||||
* forwarded to Element_fit_text as manual_size, which disables auto-scale for
|
* back via liveQuery (IDB → reactive UI update) automatically.
|
||||||
* that field and sets the size directly.
|
|
||||||
*
|
*
|
||||||
* Differences from v1 (ae_comp__badge_obj_view.svelte):
|
* Differences from v1 (ae_comp__badge_obj_view.svelte):
|
||||||
* - Imports Element_fit_text
|
* - No inline edit mode — floating Edit/Save/Cancel panel removed
|
||||||
* - Removed: full_name_class_size, professional_title_class_size,
|
* - No handle_save_changes / handle_cancel_changes / handle_print_badge
|
||||||
* affiliations_class_size, location_class_size state vars
|
* - Print button lives in ae_comp__badge_print_controls.svelte
|
||||||
* - Removed: longest_str_part() function
|
* - Uses Element_fit_text (binary-search auto-scaling) for all text fields
|
||||||
* - Removed: the $effect block that computed those size classes
|
* - display_* values are $derived directly from lq__event_badge_obj
|
||||||
* - Text fields in display mode are wrapped with <Element_fit_text>
|
* - Removed: longest_str_part() heuristic sizing
|
||||||
* - Edit mode uses plain divs (no auto-scaling on inputs)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,37 +22,29 @@
|
|||||||
event_badge_id: string;
|
event_badge_id: string;
|
||||||
lq__event_badge_obj?: any;
|
lq__event_badge_obj?: any;
|
||||||
lq__event_badge_template_obj?: any;
|
lq__event_badge_template_obj?: any;
|
||||||
update_status?: string;
|
/** Kept for API compatibility; unused in v2 (no inline edit mode). */
|
||||||
update_complete?: boolean;
|
|
||||||
is_review_mode?: boolean;
|
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;
|
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;
|
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;
|
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;
|
font_size_location?: number | null;
|
||||||
log_lvl?: number;
|
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 {
|
let {
|
||||||
event_id,
|
|
||||||
event_badge_id,
|
event_badge_id,
|
||||||
lq__event_badge_obj,
|
lq__event_badge_obj,
|
||||||
lq__event_badge_template_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_name,
|
||||||
font_size_title,
|
font_size_title,
|
||||||
font_size_affiliations,
|
font_size_affiliations,
|
||||||
font_size_location,
|
font_size_location,
|
||||||
log_lvl = 0
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Badge layout CSS — compiled in, hot-reloads in dev.
|
// 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_epson_4x5_fanfold.css';
|
||||||
import '$lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css';
|
import '$lib/ae_events/badges/css/badge_layout_zebra_zc10l_pvc.css';
|
||||||
|
|
||||||
// *** Import Svelte specific
|
|
||||||
import { browser } from '$app/environment';
|
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 { core_func } from '$lib/ae_core/ae_core_functions';
|
||||||
import {
|
import { ae_loc, slct } from '$lib/stores/ae_stores';
|
||||||
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';
|
|
||||||
|
|
||||||
// V2: auto-scaling text component
|
// V2: auto-scaling text component
|
||||||
import Element_fit_text from '$lib/elements/element_fit_text.svelte';
|
import Element_fit_text from '$lib/elements/element_fit_text.svelte';
|
||||||
|
|
||||||
// *** Variables
|
// --- Badge type list from template ---
|
||||||
let ae_triggers: key_val = $state({});
|
// Each item: { code: string, name: string }. Drives footer display + (in controls) dropdown.
|
||||||
|
|
||||||
// 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.
|
|
||||||
let badge_type_code_li = $derived.by(() => {
|
let badge_type_code_li = $derived.by(() => {
|
||||||
const raw = $lq__event_badge_template_obj?.badge_type_list;
|
const raw = $lq__event_badge_template_obj?.badge_type_list;
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
@@ -101,33 +73,50 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Human-readable name for the current badge type, printed on the badge footer.
|
// --- Effective display values (override ?? base) ---
|
||||||
// Priority: badge_type_override (staff custom name) → badge_type (base import name)
|
// $derived keeps these reactive to liveQuery updates from the right panel saves.
|
||||||
// → template badge_type_list lookup by effective code → code itself as last resort.
|
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 CSS styling) with "Member" but have a
|
||||||
// A special-case attendee can share a code (and thus styling) with "Member" but have a
|
|
||||||
// custom displayed name like "Life Member" stored in badge_type_override.
|
// custom displayed name like "Life Member" stored in badge_type_override.
|
||||||
let badge_type_name = $derived.by(() => {
|
let badge_type_name = $derived.by(() => {
|
||||||
// Staff override name takes priority
|
|
||||||
const override_name = $lq__event_badge_obj?.badge_type_override;
|
const override_name = $lq__event_badge_obj?.badge_type_override;
|
||||||
if (override_name) return override_name;
|
if (override_name) return override_name;
|
||||||
// Base name from import/registration system
|
|
||||||
const base_name = $lq__event_badge_obj?.badge_type;
|
const base_name = $lq__event_badge_obj?.badge_type;
|
||||||
if (base_name) return base_name;
|
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 }[])
|
const found = (badge_type_code_li as { code: string; name: string }[])
|
||||||
.find(item => item.code === editable_badge_type_code);
|
.find(item => item.code === effective_badge_type_code);
|
||||||
return found?.name ?? editable_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).
|
// Show badge back when duplex=1 (or not yet set — safe default for existing templates).
|
||||||
// duplex=0/false → single-sided print (e.g. Zebra ZC10L PVC cards); back section is hidden.
|
// duplex=0/false → single-sided (e.g. Zebra ZC10L PVC cards); back section is hidden.
|
||||||
let show_badge_back = $derived(
|
let show_badge_back = $derived(
|
||||||
$lq__event_badge_template_obj?.duplex == null || !!$lq__event_badge_template_obj?.duplex
|
$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_receipt = $derived(false);
|
||||||
let show_tickets = $derived(false);
|
let show_tickets = $derived(false);
|
||||||
|
|
||||||
@@ -140,18 +129,13 @@
|
|||||||
* name — height of the name Element_fit_text
|
* name — height of the name Element_fit_text
|
||||||
* title — height of the title Element_fit_text
|
* title — height of the title Element_fit_text
|
||||||
* grp_aff_loc — height of the affiliations_location container
|
* 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
|
* affiliations — height of the affiliations Element_fit_text
|
||||||
* location — height of the location Element_fit_text
|
* location — height of the location Element_fit_text
|
||||||
*
|
*
|
||||||
* Flex values: 'around' | 'between' | 'even' | 'center' | 'start' | 'end'
|
* Flex values: 'around' | 'between' | 'even' | 'center' | 'start' | 'end'
|
||||||
* (mapped to space-around, space-between, space-evenly, center, flex-start, flex-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.
|
* TODO: Move to badge template config (cfg_json) once multi-layout events are needed.
|
||||||
* Tune these values with real badge data and a ruler.
|
* Tune these values with real badge data and a ruler.
|
||||||
*/
|
*/
|
||||||
@@ -212,84 +196,40 @@
|
|||||||
return map[val] ?? 'space-around';
|
return map[val] ?? 'space-around';
|
||||||
}
|
}
|
||||||
|
|
||||||
// *** Set initial variables
|
// Sync selected badge ID to the slct store (used by other app modules for context).
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
$slct.event_badge_id = event_badge_id;
|
$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(() => {
|
$effect(() => {
|
||||||
if ($lq__event_badge_obj) {
|
if (browser && $lq__event_badge_obj?.event_badge_id) {
|
||||||
if (log_lvl) {
|
qr_error_message = '';
|
||||||
console.log('Initializing editable fields from lq__event_badge_obj');
|
qr_data_url = '';
|
||||||
}
|
const params: any = {
|
||||||
editable_full_name_override =
|
obj_type: 'event_badge',
|
||||||
$lq__event_badge_obj.full_name_override ??
|
obj_id: $lq__event_badge_obj.event_badge_id
|
||||||
$lq__event_badge_obj.full_name;
|
};
|
||||||
editable_professional_title_override =
|
try {
|
||||||
$lq__event_badge_obj.professional_title_override ??
|
qr_data_url = core_func.js_generate_qr_code('obj', params);
|
||||||
$lq__event_badge_obj.professional_title;
|
} catch (error: any) {
|
||||||
editable_affiliations_override =
|
qr_error_message = error?.message || 'Unknown error';
|
||||||
$lq__event_badge_obj.affiliations_override ??
|
console.error(error);
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let show_print_msg: null | boolean = $state(null);
|
/* *** BEGIN *** Legacy ticket/option state — move to template config in future */
|
||||||
let hide_qr: null | boolean = $state(null);
|
|
||||||
|
|
||||||
let use_badge_type_code = $state('');
|
|
||||||
let option_ticket_1_override = $state('');
|
let option_ticket_1_override = $state('');
|
||||||
let option_ticket_2_override = $state('');
|
let option_ticket_2_override = $state('');
|
||||||
let option_ticket_3_override = $state('');
|
let option_ticket_3_override = $state('');
|
||||||
let option_other_1_override = $state('');
|
let option_other_1_override = $state('');
|
||||||
let option_other_2_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_1_display_opt = 'front_bool';
|
||||||
let option_ticket_2_display_opt = 'front_bool';
|
let option_ticket_2_display_opt = 'front_bool';
|
||||||
let option_ticket_3_display_opt = 'front_bool';
|
let option_ticket_3_display_opt = 'front_bool';
|
||||||
@@ -314,227 +254,16 @@
|
|||||||
code_to_html.option_2['true'] = '<span class="fas fa-star-of-life"></span>';
|
code_to_html.option_2['true'] = '<span class="fas fa-star-of-life"></span>';
|
||||||
code_to_html.option_2['True'] = '<span class="fas fa-star-of-life"></span>';
|
code_to_html.option_2['True'] = '<span class="fas fa-star-of-life"></span>';
|
||||||
code_to_html.option_2['First Time '] = '<span class="fas fa-hand-paper"></span>';
|
code_to_html.option_2['First Time '] = '<span class="fas fa-hand-paper"></span>';
|
||||||
/* *** END *** This should be moved out */
|
/* *** END *** Legacy ticket/option state */
|
||||||
|
|
||||||
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<T extends Event>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Template debug info: screen-only. Reads from the template prop where $store subscription is reliable. -->
|
<!-- Template debug info: screen-only. Shows layout context and v2 marker. -->
|
||||||
<div class="print:hidden flex items-center gap-2 text-xs text-gray-400 font-mono mb-1">
|
<div class="print:hidden flex items-center gap-2 text-xs text-gray-400 font-mono mb-1">
|
||||||
<span title="Badge template name">{$lq__event_badge_template_obj?.name ?? '—'}</span>
|
<span title="Badge template name">{$lq__event_badge_template_obj?.name ?? '—'}</span>
|
||||||
<span class="text-gray-300">|</span>
|
<span class="text-gray-300">|</span>
|
||||||
<span title="Layout code">{$lq__event_badge_template_obj?.layout ?? '(no layout)'}</span>
|
<span title="Layout code">{$lq__event_badge_template_obj?.layout ?? '(no layout)'}</span>
|
||||||
<span class="text-gray-300">|</span>
|
<span class="text-gray-300">|</span>
|
||||||
<span class="text-blue-400" title="V2 — auto-scaling text">v2</span>
|
<span class="text-blue-400" title="V2 — auto-scaling text, display-only render">v2</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -553,7 +282,7 @@
|
|||||||
{#if $lq__event_badge_obj && $lq__event_badge_template_obj}
|
{#if $lq__event_badge_obj && $lq__event_badge_template_obj}
|
||||||
<!-- *** badge_front section start *** -->
|
<!-- *** badge_front section start *** -->
|
||||||
<section
|
<section
|
||||||
class="badge_front badge_type__{use_badge_type_code.toLowerCase()}
|
class="badge_front badge_type__{effective_badge_type_code.toLowerCase()}
|
||||||
flex flex-col gap-1
|
flex flex-col gap-1
|
||||||
items-stretch justify-between
|
items-stretch justify-between
|
||||||
min-h-[6.0in]
|
min-h-[6.0in]
|
||||||
@@ -578,160 +307,6 @@
|
|||||||
Front of badge
|
Front of badge
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
print:hidden absolute top-1 right-4
|
|
||||||
hover:preset-tonal-secondary
|
|
||||||
transition-all group
|
|
||||||
flex flex-col gap-1 items-center justify-center
|
|
||||||
"
|
|
||||||
class:preset-outlined-warning-200-800={edit_mode_active}
|
|
||||||
class:preset-tonal-warning={edit_mode_active}
|
|
||||||
>
|
|
||||||
{#if edit_mode_active}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm text-xs
|
|
||||||
preset-tonal-success preset-outlined-success-100-900 hover:preset-filled-success-500
|
|
||||||
transition-all group
|
|
||||||
"
|
|
||||||
onclick={handle_save_changes}
|
|
||||||
title="Save Changes"
|
|
||||||
data-testid="badge-save-btn"
|
|
||||||
>
|
|
||||||
<span class="fas fa-save m-1"></span>
|
|
||||||
<span
|
|
||||||
class="
|
|
||||||
hidden
|
|
||||||
group-hover:inline-block
|
|
||||||
text-xs
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Save Changes
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm text-xs
|
|
||||||
preset-tonal-tertiary preset-outlined-tertiary-100-900 hover:preset-filled-tertiary-500
|
|
||||||
transition-all group
|
|
||||||
"
|
|
||||||
onclick={handle_cancel_changes}
|
|
||||||
title="Cancel Editing"
|
|
||||||
data-testid="badge-cancel-btn"
|
|
||||||
>
|
|
||||||
<span class="fas fa-times m-1"></span>
|
|
||||||
<span
|
|
||||||
class="
|
|
||||||
hidden
|
|
||||||
group-hover:inline-block
|
|
||||||
text-xs
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm text-xs
|
|
||||||
preset-tonal-warning preset-outlined-warning-100-900 hover:preset-filled-secondary-500
|
|
||||||
transition-all group
|
|
||||||
"
|
|
||||||
onclick={() => {
|
|
||||||
edit_mode_active = true;
|
|
||||||
}}
|
|
||||||
title="Edit Badge Information"
|
|
||||||
data-testid="badge-edit-btn"
|
|
||||||
>
|
|
||||||
<span class="fas fa-edit m-1"></span>
|
|
||||||
<span
|
|
||||||
class="
|
|
||||||
hidden
|
|
||||||
group-hover:inline-block
|
|
||||||
text-xs
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Edit Badge Information
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Print Button -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm text-xs
|
|
||||||
preset-outlined-primary-100-900 hover:preset-filled-primary-500
|
|
||||||
transition-all group
|
|
||||||
"
|
|
||||||
class:preset-tonal-primary={print_status === 'loading'}
|
|
||||||
class:preset-filled-success-500={print_status === 'done'}
|
|
||||||
class:preset-filled-error-500={print_status === 'error'}
|
|
||||||
onclick={handle_print_badge}
|
|
||||||
disabled={print_status === 'loading'}
|
|
||||||
title="Print Badge (Increment Count)"
|
|
||||||
data-testid="badge-print-btn"
|
|
||||||
>
|
|
||||||
{#if print_status === 'loading'}
|
|
||||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
|
||||||
{:else if print_status === 'done'}
|
|
||||||
<span class="fas fa-check m-1"></span>
|
|
||||||
{:else if print_status === 'error'}
|
|
||||||
<span class="fas fa-exclamation-triangle m-1"></span>
|
|
||||||
{:else}
|
|
||||||
<span class="fas fa-print m-1"></span>
|
|
||||||
{/if}
|
|
||||||
<span
|
|
||||||
class="
|
|
||||||
hidden
|
|
||||||
group-hover:inline-block
|
|
||||||
text-xs
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{#if print_status === 'done'}
|
|
||||||
Printed!
|
|
||||||
{:else if print_status === 'error'}
|
|
||||||
Error
|
|
||||||
{:else}
|
|
||||||
Print Badge
|
|
||||||
{#if $lq__event_badge_obj?.print_count}
|
|
||||||
({$lq__event_badge_obj.print_count})
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="w-md max-w-lg m-1 p-1"
|
|
||||||
class:hidden={!edit_mode_active}
|
|
||||||
>
|
|
||||||
<p class="text-xs italic text-gray-500">
|
|
||||||
Show list of fields that they can edit here. This may
|
|
||||||
need to broken down in to sections that can be
|
|
||||||
collapsed.
|
|
||||||
</p>
|
|
||||||
<ul class="text-left list-disc list-inside text-sm">
|
|
||||||
<li>Full Name</li>
|
|
||||||
<li>Professional Title</li>
|
|
||||||
<li>Affiliations</li>
|
|
||||||
<li>Location</li>
|
|
||||||
<li>Option Ticket 1</li>
|
|
||||||
<li>Option Ticket 2</li>
|
|
||||||
<li>Option Ticket 3</li>
|
|
||||||
<li>Option Other 1</li>
|
|
||||||
<li>Option Other 2</li>
|
|
||||||
<li>Badge Type</li>
|
|
||||||
<li>Allow Tracking</li>
|
|
||||||
<li>Show Print Message</li>
|
|
||||||
<li>Hide QR Code</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.header_path}
|
{#if $lq__event_badge_template_obj.header_path}
|
||||||
<div
|
<div
|
||||||
class="badge_header
|
class="badge_header
|
||||||
@@ -777,6 +352,11 @@
|
|||||||
items-stretch justify-between
|
items-stretch justify-between
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<!--
|
||||||
|
person_name container: explicit height from fit_heights so Element_fit_text
|
||||||
|
can measure overflow correctly. flex-col with justify-content distributes
|
||||||
|
name and title vertically within the constrained space.
|
||||||
|
-->
|
||||||
<div
|
<div
|
||||||
class="person_name
|
class="person_name
|
||||||
m-0 p-0
|
m-0 p-0
|
||||||
@@ -787,21 +367,12 @@
|
|||||||
style="height: {fit_heights.grp_name_title}; justify-content: {flex_justify(fit_heights.grp_name_title_flex)}"
|
style="height: {fit_heights.grp_name_title}; justify-content: {flex_justify(fit_heights.grp_name_title_flex)}"
|
||||||
>
|
>
|
||||||
<!--
|
<!--
|
||||||
V2: Element_fit_text wraps display mode only.
|
V2: Element_fit_text wraps display mode only — no inline edit inputs.
|
||||||
Edit mode uses a plain div so inputs are not auto-scaled.
|
manual_size={font_size_name ?? null} disables auto-scaling when the
|
||||||
manual_size={font_size_name ?? null} disables auto-scaling
|
print-controls panel sets a fixed size. Reset in controls → null → auto resumes.
|
||||||
when the print-controls panel is setting a fixed size.
|
Bounds: min=36 (readable for short names at small containers)
|
||||||
Bounds: min=20 (readable at small sizes) max=80 (fills badge width for short names).
|
max=80 (fills badge width for short names like "Bob")
|
||||||
-->
|
-->
|
||||||
{#if edit_mode_active}
|
|
||||||
<div class="full_name_override_all leading-none">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={editable_full_name_override}
|
|
||||||
class="input w-full text-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Element_fit_text
|
<Element_fit_text
|
||||||
min={36}
|
min={36}
|
||||||
max={80}
|
max={80}
|
||||||
@@ -810,29 +381,19 @@
|
|||||||
class="full_name_override_all hover:bg-pink-100/50"
|
class="full_name_override_all hover:bg-pink-100/50"
|
||||||
>
|
>
|
||||||
<span class="full_name_override">
|
<span class="full_name_override">
|
||||||
{#if editable_full_name_override}
|
{#if display_name}
|
||||||
{@html editable_full_name_override.trim()}
|
{@html display_name.trim()}
|
||||||
{:else}
|
{:else}
|
||||||
-- no name --
|
-- no name --
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</Element_fit_text>
|
</Element_fit_text>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if editable_professional_title_override || edit_mode_active}
|
{#if display_title}
|
||||||
<!--
|
<!--
|
||||||
Bounds: min=14 (small italic fine at small sizes) max=38 (fills badge width for short titles).
|
Bounds: min=18 (small italic fine at small sizes)
|
||||||
|
max=38 (fills badge width for short titles)
|
||||||
-->
|
-->
|
||||||
{#if edit_mode_active}
|
|
||||||
<div class="professional_title leading-none italic">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={editable_professional_title_override}
|
|
||||||
class="input w-full text-center"
|
|
||||||
data-testid="badge-professional-title-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Element_fit_text
|
<Element_fit_text
|
||||||
min={18}
|
min={18}
|
||||||
max={38}
|
max={38}
|
||||||
@@ -840,13 +401,16 @@
|
|||||||
height={fit_heights.title}
|
height={fit_heights.title}
|
||||||
class="professional_title italic hover:bg-pink-100/50"
|
class="professional_title italic hover:bg-pink-100/50"
|
||||||
>
|
>
|
||||||
{@html editable_professional_title_override}
|
{@html display_title}
|
||||||
</Element_fit_text>
|
</Element_fit_text>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#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}
|
||||||
|
<!--
|
||||||
|
affiliations_location container: explicit height from fit_heights.
|
||||||
|
flex-col with justify-content distributes affiliations and location.
|
||||||
|
-->
|
||||||
<div
|
<div
|
||||||
class="affiliations_location
|
class="affiliations_location
|
||||||
m-0 p-0
|
m-0 p-0
|
||||||
@@ -857,19 +421,11 @@
|
|||||||
"
|
"
|
||||||
style="height: {fit_heights.grp_aff_loc}; justify-content: {flex_justify(fit_heights.grp_aff_loc_flex)}"
|
style="height: {fit_heights.grp_aff_loc}; justify-content: {flex_justify(fit_heights.grp_aff_loc_flex)}"
|
||||||
>
|
>
|
||||||
{#if editable_affiliations_override || edit_mode_active}
|
{#if display_affiliations}
|
||||||
<!--
|
<!--
|
||||||
Bounds: min=12 (multi-line affiliations can be small) max=40 (fills badge for short org names).
|
Bounds: min=18 (multi-line affiliations can be small)
|
||||||
|
max=40 (fills badge for short org names)
|
||||||
-->
|
-->
|
||||||
{#if edit_mode_active}
|
|
||||||
<div class="affiliations leading-none">
|
|
||||||
<textarea
|
|
||||||
bind:value={editable_affiliations_override}
|
|
||||||
class="textarea w-full text-center"
|
|
||||||
rows="2"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Element_fit_text
|
<Element_fit_text
|
||||||
min={18}
|
min={18}
|
||||||
max={40}
|
max={40}
|
||||||
@@ -877,24 +433,15 @@
|
|||||||
height={fit_heights.affiliations}
|
height={fit_heights.affiliations}
|
||||||
class="affiliations hover:bg-pink-100/50"
|
class="affiliations hover:bg-pink-100/50"
|
||||||
>
|
>
|
||||||
{@html editable_affiliations_override}
|
{@html display_affiliations}
|
||||||
</Element_fit_text>
|
</Element_fit_text>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if editable_location_override || edit_mode_active}
|
{#if display_location}
|
||||||
<!--
|
<!--
|
||||||
Bounds: min=12 (long city/country strings) max=34 (matches title upper bound).
|
Bounds: min=18 (long city/country strings)
|
||||||
|
max=34 (matches title upper bound)
|
||||||
-->
|
-->
|
||||||
{#if edit_mode_active}
|
|
||||||
<div class="location leading-none">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={editable_location_override}
|
|
||||||
class="input w-full text-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Element_fit_text
|
<Element_fit_text
|
||||||
min={18}
|
min={18}
|
||||||
max={34}
|
max={34}
|
||||||
@@ -903,37 +450,14 @@
|
|||||||
class="location hover:bg-pink-100/50"
|
class="location hover:bg-pink-100/50"
|
||||||
>
|
>
|
||||||
<span class="city state_province country"
|
<span class="city state_province country"
|
||||||
>{@html editable_location_override}</span
|
>{@html display_location}</span
|
||||||
>
|
>
|
||||||
</Element_fit_text>
|
</Element_fit_text>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if edit_mode_active}
|
|
||||||
<!-- Email: editable in review/edit mode only — never printed on badge -->
|
|
||||||
<div class="email-field text-sm">
|
|
||||||
<label>Email: <input
|
|
||||||
type="email"
|
|
||||||
bind:value={editable_email}
|
|
||||||
class="input w-full"
|
|
||||||
/></label>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if 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}
|
||||||
<!-- Allow tracking: editable in review/edit mode only — never printed on badge -->
|
|
||||||
<div class="allow-tracking-field text-sm flex items-center justify-center gap-2">
|
|
||||||
<label>Allow Tracking: <input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={editable_allow_tracking}
|
|
||||||
class="checkbox"
|
|
||||||
/></label>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/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}
|
|
||||||
<!-- flex-col so ticket icons row and QR stack vertically; QR is centered -->
|
<!-- flex-col so ticket icons row and QR stack vertically; QR is centered -->
|
||||||
<div class="special flex flex-col items-center w-full">
|
<div class="special flex flex-col items-center w-full">
|
||||||
<div class="flex flex-row justify-between w-full">
|
<div class="flex flex-row justify-between w-full">
|
||||||
@@ -972,7 +496,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="badge_footer
|
class="badge_footer
|
||||||
{editable_badge_type_code?.toLowerCase()}
|
{effective_badge_type_code.toLowerCase()}
|
||||||
justify-self-end
|
justify-self-end
|
||||||
max-h-[.50in]
|
max-h-[.50in]
|
||||||
max-w-full
|
max-w-full
|
||||||
@@ -981,23 +505,8 @@
|
|||||||
flex flex-row gap-1 items-center justify-center
|
flex flex-row gap-1 items-center justify-center
|
||||||
hover:outline-2 hover:outline-dashed hover:outline-gray-500/75
|
hover:outline-2 hover:outline-dashed hover:outline-gray-500/75
|
||||||
"
|
"
|
||||||
title={editable_badge_type_code}
|
title={effective_badge_type_code}
|
||||||
>
|
>
|
||||||
{#if edit_mode_active && badge_type_code_li.length > 0}
|
|
||||||
<label
|
|
||||||
>Badge Type:
|
|
||||||
<select
|
|
||||||
bind:value={editable_badge_type_code}
|
|
||||||
class="select text-xs px-1 max-w-fit"
|
|
||||||
>
|
|
||||||
{#each badge_type_code_li as badge_type_code_item (badge_type_code_item.code)}
|
|
||||||
<option value={badge_type_code_item.code}
|
|
||||||
>{badge_type_code_item.name}</option
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{:else}
|
|
||||||
{#if option_other_1_override && ['front_bool', 'front_back_bool'].includes(option_other_1_display_opt)}
|
{#if option_other_1_override && ['front_bool', 'front_back_bool'].includes(option_other_1_display_opt)}
|
||||||
<span class="badge_footer_special_left"
|
<span class="badge_footer_special_left"
|
||||||
><span class="fas fa-biohazard"></span></span
|
><span class="fas fa-biohazard"></span></span
|
||||||
@@ -1011,7 +520,7 @@
|
|||||||
<!-- badge_type_code is the CSS class hook for per-event stylesheets;
|
<!-- badge_type_code is the CSS class hook for per-event stylesheets;
|
||||||
badge_type_name is what's actually printed on the badge. -->
|
badge_type_name is what's actually printed on the badge. -->
|
||||||
<span
|
<span
|
||||||
class="badge_footer_center {editable_badge_type_code?.toLowerCase()}"
|
class="badge_footer_center {effective_badge_type_code.toLowerCase()}"
|
||||||
>{badge_type_name}</span
|
>{badge_type_name}</span
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -1024,7 +533,6 @@
|
|||||||
>{@html option_other_2_override}</span
|
>{@html option_other_2_override}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- badge class div end -->
|
<!-- badge class div end -->
|
||||||
@@ -1238,36 +746,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.exhibitor_info}
|
|
||||||
<div class="container exhibitor_information">
|
|
||||||
<strong>Exhibitor Info:</strong>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.presenter_info}
|
|
||||||
<div class="container presenter_information">
|
|
||||||
<strong>Presenter Info:</strong>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.staff_info}
|
|
||||||
<div class="container staff_information">
|
|
||||||
<strong>Staff Info:</strong>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.vip_info}
|
|
||||||
<div class="container vip_information">
|
|
||||||
<strong>VIP Info:</strong>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.vote_info}
|
|
||||||
<div class="container vote_information">
|
|
||||||
<div><strong>Voting Info:</strong></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lq__event_badge_template_obj.show_qr_back}
|
{#if $lq__event_badge_template_obj.show_qr_back}
|
||||||
<div
|
<div
|
||||||
class="person_information qr_badge_id
|
class="person_information qr_badge_id
|
||||||
@@ -1448,7 +926,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<!-- *** ticket section end *** -->
|
<!-- *** ticket section end *** -->
|
||||||
{/if}
|
{/if}
|
||||||
<!-- End if for $lq__event_badge_template_obj -->
|
<!-- End if for lq__event_badge_obj && lq__event_badge_template_obj -->
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
import type { key_val } from '$lib/stores/ae_stores';
|
import type { key_val } from '$lib/stores/ae_stores';
|
||||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||||
import { events_func } from '$lib/ae_events_functions';
|
import { events_func } from '$lib/ae_events_functions';
|
||||||
import { Pencil, Check, X, LoaderCircle, ChevronDown } from 'lucide-svelte';
|
import { browser } from '$app/environment';
|
||||||
|
import { Pencil, Check, X, LoaderCircle, ChevronDown, Printer } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event_id: string;
|
event_id: string;
|
||||||
@@ -55,6 +56,58 @@
|
|||||||
// professional_title_override, affiliations_override, and location_override.
|
// professional_title_override, affiliations_override, and location_override.
|
||||||
let is_trusted = $derived($ae_loc.trusted_access === true);
|
let is_trusted = $derived($ae_loc.trusted_access === true);
|
||||||
|
|
||||||
|
// IMPORTANT: $ae_loc.edit_mode is the GLOBAL AE Edit Mode — a UI preference that
|
||||||
|
// reveals editable fields, debug info, and advanced options across the whole app.
|
||||||
|
// It is NOT the same as the badge-specific edit state (which no longer exists in
|
||||||
|
// the v2 badge render — all badge editing happens here in the controls panel).
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// --- Print ---
|
||||||
|
let print_count = $derived($lq__event_badge_obj?.print_count ?? 0);
|
||||||
|
let is_printed = $derived(print_count >= 1);
|
||||||
|
|
||||||
|
// Print is available to Trusted+ when: not yet printed, OR global edit mode is on (reprint).
|
||||||
|
let can_print = $derived(is_trusted && (!is_printed || is_global_edit_mode));
|
||||||
|
|
||||||
|
type PrintStatus = 'idle' | 'loading' | 'done' | 'error';
|
||||||
|
let print_status: PrintStatus = $state('idle');
|
||||||
|
|
||||||
|
async function handle_print_badge() {
|
||||||
|
if (!$lq__event_badge_obj?.event_badge_id) return;
|
||||||
|
print_status = 'loading';
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const is_first_print = print_count === 0;
|
||||||
|
const data_to_update: key_val = {
|
||||||
|
print_count: 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_badge_id: $lq__event_badge_obj.event_badge_id,
|
||||||
|
data_kv: data_to_update,
|
||||||
|
log_lvl
|
||||||
|
});
|
||||||
|
print_status = 'done';
|
||||||
|
// Trigger browser print dialog after count is recorded
|
||||||
|
if (browser) window.print();
|
||||||
|
// Brief success flash, then return to badge search
|
||||||
|
await new Promise<void>(r => setTimeout(r, 1000));
|
||||||
|
// Full navigation back to badge search — avoids goto() lint rule in child components
|
||||||
|
if (browser) window.location.href = `/events/${event_id}/badges`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Badge print controls: print error:', err);
|
||||||
|
print_status = 'error';
|
||||||
|
setTimeout(() => { print_status = 'idle'; }, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Badge type list from template ---
|
// --- Badge type list from template ---
|
||||||
// Each item: { code: string, name: string }. Used for the badge type dropdown.
|
// Each item: { code: string, name: string }. Used for the badge type dropdown.
|
||||||
let badge_type_code_li = $derived.by((): { code: string; name: string }[] => {
|
let badge_type_code_li = $derived.by((): { code: string; name: string }[] => {
|
||||||
@@ -265,6 +318,45 @@
|
|||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
Main panel
|
Main panel
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
|
|
||||||
|
<!-- Print button — canonical print action for v2. Increments print_count, fires
|
||||||
|
window.print(), then navigates back to badge search. Only shown to Trusted+
|
||||||
|
when not yet printed, OR when global AE Edit Mode is active (allows reprints).
|
||||||
|
The header "Print Now" button is a shortcut that calls window.print() only —
|
||||||
|
it does NOT track print count. Always use this button for the official print. -->
|
||||||
|
{#if can_print}
|
||||||
|
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm w-full flex items-center justify-center gap-2"
|
||||||
|
class:preset-filled-primary={print_status === 'idle'}
|
||||||
|
class:preset-tonal-surface={print_status === 'loading'}
|
||||||
|
class:preset-filled-success={print_status === 'done'}
|
||||||
|
class:preset-tonal-error={print_status === 'error'}
|
||||||
|
onclick={handle_print_badge}
|
||||||
|
disabled={print_status === 'loading' || print_status === 'done'}
|
||||||
|
title={is_printed ? `Reprint badge (printed ${print_count}×)` : 'Print badge now'}
|
||||||
|
data-testid="badge-print-btn"
|
||||||
|
>
|
||||||
|
{#if print_status === 'loading'}
|
||||||
|
<LoaderCircle size="14" class="animate-spin" /> Printing…
|
||||||
|
{:else if print_status === 'done'}
|
||||||
|
<Check size="14" /> Printed!
|
||||||
|
{:else if print_status === 'error'}
|
||||||
|
Error — try again
|
||||||
|
{:else}
|
||||||
|
<Printer size="14" />
|
||||||
|
{is_printed ? `Reprint (${print_count}×)` : 'Print Badge'}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if is_printed && print_status === 'idle'}
|
||||||
|
<p class="text-[10px] text-amber-600 dark:text-amber-400 text-center mt-1">
|
||||||
|
Already printed {print_count}×
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
|
|
||||||
<!-- === NAME === -->
|
<!-- === NAME === -->
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
|
|
||||||
// Access level shortcuts
|
// Access level shortcuts
|
||||||
let is_trusted = $derived($ae_loc.trusted_access === true);
|
let is_trusted = $derived($ae_loc.trusted_access === true);
|
||||||
|
|
||||||
|
// IMPORTANT: $ae_loc.edit_mode is the GLOBAL AE Edit Mode — a UI preference that
|
||||||
|
// reveals editable fields, debug info, and advanced options app-wide. It is NOT
|
||||||
|
// a badge-specific edit toggle. Never write to it from badge components.
|
||||||
|
// Used here only to gate the header "Print Now" shortcut (allow reprints in edit mode).
|
||||||
let is_edit_mode = $derived($ae_loc.edit_mode === true);
|
let is_edit_mode = $derived($ae_loc.edit_mode === true);
|
||||||
|
|
||||||
// Print state derived from badge
|
// Print state derived from badge
|
||||||
@@ -186,7 +191,11 @@
|
|||||||
{use_v2 ? 'v2' : 'v1'}
|
{use_v2 ? 'v2' : 'v1'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 1. Print Now: Trusted+, not printed OR Edit Mode (reprint) -->
|
<!-- 1. Print Now (header shortcut): calls window.print() only — does NOT track
|
||||||
|
print_count. The canonical "Print Badge" button in the right panel controls
|
||||||
|
increments the count, fires window.print(), then redirects to badge search.
|
||||||
|
Use this header button only as a quick re-trigger of the print dialog. -->
|
||||||
|
<!-- Trusted+, not printed OR global Edit Mode active (reprint) -->
|
||||||
{#if is_trusted && (!is_printed || is_edit_mode)}
|
{#if is_trusted && (!is_printed || is_edit_mode)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user