Implement dynamic Custom Questions editor in Lead Detail view

- Added 'Comp_lead_detail_form' for editing licensee responses.
- Implemented reactive form generation based on Exhibit question definitions.
- Wired up 'Edit Mode' toggle in Lead Detail page.
- Added CRUD editors for lead notes, priority, and visibility status.
This commit is contained in:
Scott Idem
2026-02-08 23:06:09 -05:00
parent 111ef76d14
commit 1dd80cc974
2 changed files with 142 additions and 17 deletions

View File

@@ -9,6 +9,7 @@
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 Comp_lead_detail_form from './ae_comp__lead_detail_form.svelte';
import {
User,
Mail,
@@ -148,15 +149,22 @@
</div>
<!-- Custom Responses Section -->
{#if $lq__lead_obj.responses_json}
{@const responses = typeof $lq__lead_obj.responses_json === 'string' ? JSON.parse($lq__lead_obj.responses_json) : $lq__lead_obj.responses_json}
{#if Object.keys(responses).length > 0}
<div class="card p-6 space-y-4 shadow-md">
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-3">
<ListTodo size="1.2em" class="text-primary-500" />
<h3 class="text-lg font-bold uppercase tracking-wider">Custom Responses</h3>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="card p-6 space-y-4 shadow-md">
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-3">
<ListTodo size="1.2em" class="text-primary-500" />
<h3 class="text-lg font-bold uppercase tracking-wider">Custom Responses / Qualifiers</h3>
</div>
{#if is_edit_mode}
<Comp_lead_detail_form
exhibit_tracking_id={exhibit_tracking_id ?? ''}
custom_questions_json={$lq__exhibit_obj?.leads_custom_questions_json ?? '[]'}
current_responses_json={$lq__lead_obj.responses_json ?? '{}'}
/>
{:else if $lq__lead_obj.responses_json}
{@const responses = typeof $lq__lead_obj.responses_json === 'string' ? JSON.parse($lq__lead_obj.responses_json) : $lq__lead_obj.responses_json}
{#if Object.keys(responses).length > 0}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 animate-in fade-in">
{#each Object.entries(responses) as [question, answer]}
<div class="p-3 bg-surface-500/5 rounded-lg border border-surface-500/10">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest mb-1 leading-tight">{question}</div>
@@ -164,9 +172,13 @@
</div>
{/each}
</div>
</div>
{:else}
<p class="text-center opacity-30 italic py-4">No responses captured for this lead.</p>
{/if}
{:else}
<p class="text-center opacity-30 italic py-4">No responses captured for this lead.</p>
{/if}
{/if}
</div>
<!-- Notes Section -->
<div class="card p-6 space-y-4 shadow-md">

View File

@@ -1,11 +1,124 @@
<script lang="ts">
/**
* src/routes/events/[event_id]/(leads)/leads/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte
* Lead Detail Form Stub.
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte
* Lead Detail Form - Dynamic Custom Questions Editor.
*/
import { untrack } from 'svelte';
import { ae_api } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events_functions';
import { Save, LoaderCircle, CheckCircle2 } from 'lucide-svelte';
interface Props {
exhibit_tracking_id: string;
custom_questions_json?: string; // From event_exhibit
current_responses_json?: string; // From event_exhibit_tracking
}
let { exhibit_tracking_id, custom_questions_json = '[]', current_responses_json = '{}' }: Props = $props();
let question_defs: any[] = $state([]);
let responses: Record<string, any> = $state({});
let status = $state('idle'); // idle, saving, success
// Initialize data
$effect(() => {
try {
// Handle both string and pre-parsed array/object
question_defs = typeof custom_questions_json === 'string' ? JSON.parse(custom_questions_json || '[]') : (custom_questions_json || []);
const parsed_responses = typeof current_responses_json === 'string' ? JSON.parse(current_responses_json || '{}') : (current_responses_json || {});
untrack(() => {
responses = parsed_responses;
});
} catch (e) {
console.error('Failed to parse questions/responses', e);
}
});
async function handle_save() {
if (!exhibit_tracking_id) return;
status = 'saving';
try {
await events_func.update_ae_obj__exhibit_tracking({
api_cfg: $ae_api,
exhibit_tracking_id: exhibit_tracking_id,
data: {
responses_json: JSON.stringify(responses)
}
});
status = 'success';
setTimeout(() => status = 'idle', 2000);
} catch (e) {
console.error('Failed to update responses', e);
status = 'idle';
}
}
</script>
<div class="lead-detail-form p-4 card">
<h3 class="h3">Lead Details</h3>
<p>Placeholder for qualifiers and notes.</p>
</div>
<div class="lead-detail-form space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
{#each question_defs as q}
<div class="space-y-2">
<label class="label">
<span class="text-[10px] uppercase font-black opacity-40 tracking-widest ml-1">{q.label}</span>
{#if q.type === 'textarea'}
<textarea
bind:value={responses[q.label]}
class="textarea variant-filled-surface rounded-lg p-3 text-sm"
rows="3"
placeholder="Type response..."
></textarea>
{:else if q.type === 'toggle'}
<div class="flex items-center gap-4 p-3 variant-soft rounded-lg">
<input
type="checkbox"
bind:checked={responses[q.label]}
class="checkbox"
/>
<span class="text-sm font-bold">{responses[q.label] ? 'Yes' : 'No'}</span>
</div>
{:else if q.type === 'select'}
<select
bind:value={responses[q.label]}
class="select variant-filled-surface rounded-lg p-3 text-sm"
>
<option value="">-- Select Option --</option>
{#each (q.options || '').split(',').map((o: string) => o.trim()) as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
type="text"
bind:value={responses[q.label]}
class="input variant-filled-surface rounded-lg p-3 text-sm"
placeholder="Type response..."
/>
{/if}
</label>
</div>
{/each}
</div>
{#if question_defs.length === 0}
<p class="text-center opacity-30 italic py-4">No custom questions configured for this exhibit.</p>
{/if}
<button
class="btn variant-filled-primary w-full font-bold shadow-lg"
disabled={status === 'saving'}
onclick={handle_save}
>
{#if status === 'saving'}
<LoaderCircle size="1.2em" class="animate-spin mr-2" /> Saving...
{:else if status === 'success'}
<CheckCircle2 size="1.2em" class="mr-2" /> Saved!
{:else}
<Save size="1.2em" class="mr-2" /> Save Responses
{/if}
</button>
</div>