diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte index 1e418220..90718648 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte @@ -4,13 +4,12 @@ * Lead Detail View - Basic Read-Only version. */ import { page } from '$app/state'; -import { goto } from '$app/navigation'; +import { beforeNavigate, goto } from '$app/navigation'; import { liveQuery } from 'dexie'; import { db_events } from '$lib/ae_events/db_events'; import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_api, ae_loc } from '$lib/stores/ae_stores'; import { events_func } from '$lib/ae_events/ae_events_functions'; -import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte'; import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte'; import Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte'; import { @@ -29,9 +28,9 @@ import { ShieldCheck, SquarePen, Star, - Store, Trash2, - User + User, + UserCheck } from '@lucide/svelte'; const exhibit_tracking_id = $derived(page.params.exhibit_tracking_id); @@ -52,6 +51,51 @@ let lq__exhibit_obj = $derived( let is_edit_mode = $state(false); +// Priority toggle — one-click, saves immediately without entering edit mode. +let priority_saving = $state(false); +async function toggle_priority() { + if (!exhibit_tracking_id || priority_saving) return; + priority_saving = true; + try { + await events_func.update_ae_obj__exhibit_tracking({ + api_cfg: $ae_api, + exhibit_id: page.params.exhibit_id ?? '', + exhibit_tracking_id, + data: { priority: !$lq__lead_obj?.priority } + }); + } finally { + priority_saving = false; + } +} + +// Human-readable label for external_person_id. +// Raw values: 'shared_passcode' (booth shared login), an email (licensed user), +// or an Aether access_type string (trusted/administrator — staff bypass). +function format_captured_by(id: string | null | undefined): string { + if (!id) return 'Unknown'; + if (id === 'shared_passcode') return 'Booth (Shared)'; + if (id.includes('@')) return id; + const map: Record = { + trusted: 'Staff', + administrator: 'Admin', + edit_mode: 'Staff' + }; + return map[id] ?? id; +} + +// Unsaved-changes guard — warn before navigating away if notes or responses are dirty. +let form_is_dirty = $state(false); +beforeNavigate(({ cancel }) => { + const notes_dirty = + is_edit_mode && + draft_notes !== ($lq__lead_obj?.exhibitor_notes ?? ''); + if (notes_dirty || form_is_dirty) { + if (!confirm('You have unsaved changes. Leave without saving?')) { + cancel(); + } + } +}); + // Notes editing state let draft_notes = $state(''); let notes_status = $state<'idle' | 'saving' | 'success' | 'error'>('idle'); @@ -151,6 +195,58 @@ function format_date(date: any) {
{#if $lq__lead_obj} + + {#if is_edit_mode && $lq__lead_obj.enable} + {#if remove_status === 'confirm'} + + + {:else} + + {/if} + {/if} + + + + + - - - {#if $lq__lead_obj.enable} - {#if remove_status === 'confirm'} - - - {:else} - - {/if} - {/if} - {/if} - - {#if $lq__lead_obj?.priority} - - - Priority - {/if}
@@ -258,6 +314,15 @@ function format_date(date: any) { $lq__lead_obj.event_badge_affiliations_override} {/if} + {#if $lq__lead_obj.event_badge_location_override || $lq__lead_obj.event_badge_location || $lq__lead_obj.event_badge_country} +
+ + {[ + $lq__lead_obj.event_badge_location_override || $lq__lead_obj.event_badge_location, + $lq__lead_obj.event_badge_country + ].filter(Boolean).join(', ')} +
+ {/if}
{/if}
+
+ + Captured by {format_captured_by($lq__lead_obj.external_person_id)} +
@@ -300,10 +370,12 @@ function format_date(date: any) { + '{}'} + bind:is_dirty={form_is_dirty} /> {:else if $lq__lead_obj.responses_json} {@const responses = typeof $lq__lead_obj.responses_json === 'string' @@ -412,77 +484,38 @@ function format_date(date: any) {
- -
-
- -

- Exhibit Context -

-
-
- {#if is_edit_mode} -
- Exhibit Name - {$lq__lead_obj.event_exhibit_name || - '...'} -
- {/if} -
- Captured By - {$lq__lead_obj.external_person_id || - 'Unknown'} -
- - {#if is_edit_mode} -
- Priority Lead - -
- {/if} -
-
- - -
+ + {#if $ae_loc.manager_access}
- System Audit -
-
-
- LEAD ID: - {$lq__lead_obj.event_exhibit_tracking_id} + class="card bg-surface-500/5 space-y-4 p-5 font-mono text-[10px] opacity-60 shadow-inner"> +
+ System Audit
-
- BADGE ID: - {$lq__lead_obj.event_badge_id} -
-
- PERSON ID: - {$lq__lead_obj.event_person_id} -
-
- MODIFIED: - {format_date($lq__lead_obj.updated_on)} +
+
+ LEAD ID: + {$lq__lead_obj.event_exhibit_tracking_id} +
+
+ BADGE ID: + {$lq__lead_obj.event_badge_id} +
+
+ PERSON ID: + {$lq__lead_obj.event_person_id} +
+
+ EXHIBIT: + {$lq__lead_obj.event_exhibit_name || '—'} +
+
+ MODIFIED: + {format_date($lq__lead_obj.updated_on)} +
-
+ {/if} diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte index df16bafa..0ccf4459 100644 --- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte +++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte @@ -19,21 +19,27 @@ * Both are handled transparently. */ import { untrack } from 'svelte'; +import { goto } from '$app/navigation'; import { ae_api } from '$lib/stores/ae_stores'; import { events_func } from '$lib/ae_events/ae_events_functions'; -import { CircleCheck, LoaderCircle, Save } from '@lucide/svelte'; +import { leads_loc } from '$lib/stores/ae_events_stores__leads.svelte'; +import { ArrowRight, CircleCheck, LoaderCircle, Save, Settings } from '@lucide/svelte'; interface Props { exhibit_tracking_id: string; exhibit_id: string; + event_id: string; // For navigate-to-manage link custom_questions_json?: string; // From event_exhibit current_responses_json?: string; // From event_exhibit_tracking + is_dirty?: boolean; // Bindable — parent reads this for unsaved-changes guard } let { exhibit_tracking_id, exhibit_id, + event_id, custom_questions_json = '[]', - current_responses_json = '{}' + current_responses_json = '{}', + is_dirty = $bindable(false) }: Props = $props(); let question_defs: any[] = $state([]); @@ -42,6 +48,9 @@ let question_defs: any[] = $state([]); let flat_responses: Record = $state({}); let status = $state('idle'); // idle, saving, success +// Snapshot of responses as last saved (or as loaded). Used for dirty detection. +let saved_snapshot = $state('{}'); + $effect(() => { try { const defs = @@ -69,12 +78,18 @@ $effect(() => { } } flat_responses = flat; + saved_snapshot = JSON.stringify(flat); }); } catch (e) { console.error('Failed to parse questions/responses', e); } }); +// Expose dirty state to parent so the unsaved-changes guard can check it. +$effect(() => { + is_dirty = JSON.stringify(flat_responses) !== saved_snapshot; +}); + // Resolve the key for a question def (new: q.code, legacy: q.label) function q_key(q: any): string { return q.code || q.label || ''; @@ -97,6 +112,7 @@ async function handle_save() { responses_json: JSON.stringify(nested) } }); + saved_snapshot = JSON.stringify(flat_responses); // mark clean status = 'success'; setTimeout(() => (status = 'idle'), 2000); } catch (e) { @@ -166,21 +182,37 @@ async function handle_save() {
{#if question_defs.length === 0} -

- No custom questions configured for this exhibit. -

+ +
+

+ No custom questions configured for this exhibit yet. +

+ +
+ {:else} + {/if} - -