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_util } from '$lib/ae_utils/ae_utils';
|
||||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||||
import Element_ae_crud_v2 from '$lib/elements/element_ae_crud_v2.svelte';
|
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 {
|
import {
|
||||||
User,
|
User,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -148,15 +149,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Responses Section -->
|
<!-- Custom Responses Section -->
|
||||||
{#if $lq__lead_obj.responses_json}
|
<div class="card p-6 space-y-4 shadow-md">
|
||||||
{@const responses = typeof $lq__lead_obj.responses_json === 'string' ? JSON.parse($lq__lead_obj.responses_json) : $lq__lead_obj.responses_json}
|
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-3">
|
||||||
{#if Object.keys(responses).length > 0}
|
<ListTodo size="1.2em" class="text-primary-500" />
|
||||||
<div class="card p-6 space-y-4 shadow-md">
|
<h3 class="text-lg font-bold uppercase tracking-wider">Custom Responses / Qualifiers</h3>
|
||||||
<div class="flex items-center gap-2 border-b border-surface-500/10 pb-3">
|
</div>
|
||||||
<ListTodo size="1.2em" class="text-primary-500" />
|
|
||||||
<h3 class="text-lg font-bold uppercase tracking-wider">Custom Responses</h3>
|
{#if is_edit_mode}
|
||||||
</div>
|
<Comp_lead_detail_form
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
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]}
|
{#each Object.entries(responses) as [question, answer]}
|
||||||
<div class="p-3 bg-surface-500/5 rounded-lg border border-surface-500/10">
|
<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>
|
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest mb-1 leading-tight">{question}</div>
|
||||||
@@ -164,9 +172,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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}
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="card p-6 space-y-4 shadow-md">
|
<div class="card p-6 space-y-4 shadow-md">
|
||||||
|
|||||||
@@ -1,11 +1,124 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* src/routes/events/[event_id]/(leads)/leads/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte
|
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/ae_comp__lead_detail_form.svelte
|
||||||
* Lead Detail Form Stub.
|
* 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>
|
</script>
|
||||||
|
|
||||||
<div class="lead-detail-form p-4 card">
|
<div class="lead-detail-form space-y-6">
|
||||||
<h3 class="h3">Lead Details</h3>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<p>Placeholder for qualifiers and notes.</p>
|
{#each question_defs as q}
|
||||||
</div>
|
<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