Harden Licensed User dropdown logic and resolve data parsing errors

- Implemented dual-format parsing for 'license_li_json' (handles raw string and IDB objects).
- Added fallback lookup by 'event_exhibit_id_random' for robust record resolution.
- Fixed reactivity for licensee dropdown population.
- Hardened default selection logic based on Aether access levels.
This commit is contained in:
Scott Idem
2026-02-08 22:25:56 -05:00
parent 68075d37a1
commit 8787f8c2ff
4 changed files with 143 additions and 50 deletions

View File

@@ -57,8 +57,13 @@
} else {
// 2. Licensed User logic
try {
const licenses = JSON.parse($lq__exhibit_obj.license_li_json || '[]');
const found = licenses.find((l: any) => l.email.toLowerCase() === email.toLowerCase().trim());
// Determine raw JSON string
const raw_json = $lq__exhibit_obj?.license_li_json;
// Parse if string, otherwise use empty array
const licenses = typeof raw_json === 'string' ? JSON.parse(raw_json || '[]') : (Array.isArray(raw_json) ? raw_json : []);
const found = licenses.find((l: any) => l.email?.toLowerCase() === email.toLowerCase().trim());
if (found && found.passcode === user_passcode) {
// SUCCESS

View File

@@ -24,7 +24,16 @@
let exhibit_obj: any = $state(null);
onMount(() => {
const observable = liveQuery(() => db_events.exhibit.get(exhibit_id));
const observable = liveQuery(async () => {
if (!exhibit_id) return null;
// 1. Try primary lookup
let res = await db_events.exhibit.get(exhibit_id);
// 2. Fallback to random ID index
if (!res) {
res = await db_events.exhibit.where('event_exhibit_id_random').equals(exhibit_id).first();
}
return res;
});
const subscription = observable.subscribe((value) => {
exhibit_obj = value;
});
@@ -34,23 +43,30 @@
// Reactive list derived from the exhibit state (Licensed Exhibit Leads Users)
let licensee_li = $derived.by(() => {
try {
const licenses = JSON.parse(exhibit_obj?.license_li_json || '[]');
return Array.isArray(licenses) ? licenses : [];
const raw = exhibit_obj?.license_li_json;
if (!raw) return [];
// If it's already an array, return it. If it's a string, parse it.
if (Array.isArray(raw)) return raw;
if (typeof raw === 'string') return JSON.parse(raw || '[]');
return [];
} catch (e) {
console.error('Failed to parse licensee_li_json', e);
return [];
}
});
// Default selection logic: Aether Admins go to "all", Licensees go to "my"
$effect(() => {
// Wait for object to load
// Wait for object to load and check if initialized
if (!exhibit_obj) return;
if ($events_loc.leads.tracking__qry__licensee_email === 'all' && !$ae_loc.administrator_access) {
untrack(() => {
untrack(() => {
if ($events_loc.leads.tracking__qry__licensee_email === 'all' && !$ae_loc.administrator_access) {
$events_loc.leads.tracking__qry__licensee_email = 'my';
});
}
}
});
});
function handle_search_trigger() {

View File

@@ -220,36 +220,38 @@
</div>
<div class="card p-0 divide-y divide-surface-500/10 overflow-hidden shadow-md">
<!-- Licenses -->
<div class="p-0">
<button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group"
onclick={() => show_license_mgmt = !show_license_mgmt}
>
<div class="flex items-center gap-4">
<div class="bg-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Staff Licenses</div>
<div class="text-xs opacity-50">Manage assigned staff and codes</div>
<!-- Licenses (Administrator Access Only for now) -->
{#if $ae_loc.administrator_access}
<div class="p-0">
<button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group"
onclick={() => show_license_mgmt = !show_license_mgmt}
>
<div class="flex items-center gap-4">
<div class="bg-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Exhibit Staff Licenses</div>
<div class="text-xs opacity-50">Manage assigned staff and codes (Admin Only)</div>
</div>
</div>
</div>
{#if show_license_mgmt}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
{/if}
</button>
{#if show_license_mgmt}
<ChevronDown size="1.2em" class="opacity-20" />
{:else}
<ChevronRight size="1.2em" class="opacity-20 group-hover:translate-x-1 transition-transform" />
<div class="p-4 bg-surface-500/5 border-t border-surface-500/10 animate-in fade-in slide-in-from-top-2">
<Comp_exhibit_license_list
{exhibit_id}
license_li_json={$lq__exhibit_obj?.license_li_json ?? '[]'}
license_max={$lq__exhibit_obj?.license_max}
/>
</div>
{/if}
</button>
{#if show_license_mgmt}
<div class="p-4 bg-surface-500/5 border-t border-surface-500/10 animate-in fade-in slide-in-from-top-2">
<Comp_exhibit_license_list
{exhibit_id}
license_li_json={$lq__exhibit_obj?.license_li_json ?? '[]'}
license_max={$lq__exhibit_obj?.license_max}
/>
</div>
{/if}
</div>
</div>
{/if}
<!-- Custom Questions -->
<div class="p-0">

View File

@@ -7,6 +7,8 @@
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 Element_ae_crud_v2 from '$lib/elements/element_ae_crud_v2.svelte';
import {
User,
Mail,
@@ -20,7 +22,9 @@
ShieldCheck,
Star,
LoaderCircle,
ListTodo
ListTodo,
Edit,
Eye
} from 'lucide-svelte';
const exhibit_tracking_id = $derived(page.params.exhibit_tracking_id);
@@ -32,6 +36,16 @@
})
);
let lq__exhibit_obj = $derived(
liveQuery(async () => {
const exhibit_id = page.params.exhibit_id;
if (!exhibit_id) return null;
return await db_events.exhibit.get(exhibit_id);
})
);
let is_edit_mode = $state(false);
// Helper to format date using Aether utility
function format_date(date: any) {
if (!date) return '';
@@ -57,12 +71,29 @@
<h1 class="text-lg font-bold">Lead Profile</h1>
</div>
{#if $lq__lead_obj?.priority}
<span class="badge variant-filled-warning font-bold flex items-center gap-1">
<Star size="1em" fill="currentColor" />
Priority
</span>
{/if}
<div class="flex items-center gap-2">
{#if $lq__lead_obj}
<button
class="btn btn-sm"
class:variant-filled-primary={is_edit_mode}
class:variant-ghost-surface={!is_edit_mode}
onclick={() => is_edit_mode = !is_edit_mode}
>
{#if is_edit_mode}
<Eye size="1.2em" class="mr-1" /> View
{:else}
<Edit size="1.2em" class="mr-1" /> Edit
{/if}
</button>
{/if}
{#if $lq__lead_obj?.priority}
<span class="badge variant-filled-warning font-bold flex items-center gap-1">
<Star size="1em" fill="currentColor" />
Priority
</span>
{/if}
</div>
</header>
<div class="w-full max-w-5xl p-4 sm:p-6 space-y-6">
@@ -144,7 +175,18 @@
<h3 class="text-lg font-bold uppercase tracking-wider">Exhibitor Notes</h3>
</div>
<div class="bg-surface-500/5 p-5 rounded-xl border border-surface-500/10 min-h-[120px]">
{#if $lq__lead_obj.exhibitor_notes}
{#if is_edit_mode}
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id}
field_name="exhibitor_notes"
field_type="textarea"
current_field_value={$lq__lead_obj.exhibitor_notes}
textarea_rows={6}
display_block={true}
/>
{:else if $lq__lead_obj.exhibitor_notes}
<p class="whitespace-pre-wrap leading-relaxed">{$lq__lead_obj.exhibitor_notes}</p>
{:else}
<div class="h-full flex items-center justify-center italic opacity-30 text-sm">
@@ -172,6 +214,20 @@
<span class="text-sm opacity-60">Captured By</span>
<span class="font-mono text-[10px]">{$lq__lead_obj.external_person_id || 'Unknown'}</span>
</div>
{#if is_edit_mode}
<div class="flex justify-between items-center pt-2 border-t border-surface-500/10">
<span class="text-xs opacity-60 font-bold">Priority Lead</span>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id}
field_name="priority"
field_type="boolean"
current_field_value={$lq__lead_obj.priority}
/>
</div>
{/if}
</div>
</div>
@@ -188,11 +244,25 @@
<!-- Status Indicator -->
<div class="card p-4 flex items-center gap-3 variant-soft shadow-sm">
<ShieldCheck size="1.5em" class={$lq__lead_obj.enable ? 'text-success-500' : 'text-error-500'} />
<div>
<div class="font-bold">{$lq__lead_obj.enable ? 'Record Enabled' : 'Record Disabled'}</div>
<div class="text-[10px] opacity-50 uppercase font-black">Visibility Status</div>
</div>
{#if is_edit_mode}
<div class="flex-1 flex items-center justify-between">
<div class="font-bold text-sm">Enabled</div>
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id}
field_name="enable"
field_type="boolean"
current_field_value={$lq__lead_obj.enable}
/>
</div>
{:else}
<ShieldCheck size="1.5em" class={$lq__lead_obj.enable ? 'text-success-500' : 'text-error-500'} />
<div>
<div class="font-bold">{$lq__lead_obj.enable ? 'Record Enabled' : 'Record Disabled'}</div>
<div class="text-[10px] opacity-50 uppercase font-black">Visibility Status</div>
</div>
{/if}
</div>
</div>