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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user