add quick leads
All checks were successful
Build and Publish / build-release (push) Successful in 4m11s
All checks were successful
Build and Publish / build-release (push) Successful in 4m11s
This commit is contained in:
@@ -188,7 +188,7 @@ const customerPolicies = computed(() => policiesData.value?.data ?? [])
|
||||
<span>Customer Information</span>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-gray-600">
|
||||
<p>This customer has {{ customerPolicies.length }} policy{{ customerPolicies.length !== 1 ? 'ies' : '' }} on file.</p>
|
||||
<p>This customer has {{ customerPolicies.length }} polic{{ customerPolicies.length === 1 ? 'y' : 'ies' }} on file.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,11 +17,22 @@ const emails = ref<Record<string, string>>({
|
||||
|
||||
const roles = ['quotes', 'claims', 'renewals', 'billing', 'support']
|
||||
|
||||
const label = computed(() => {
|
||||
const getProviderLabel = computed(() => {
|
||||
if (!provider.value) return ''
|
||||
return provider.value.name || 'Unknown'
|
||||
})
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
quotes: 'Quotes',
|
||||
claims: 'Claims',
|
||||
renewals: 'Renewals',
|
||||
billing: 'Billing',
|
||||
support: 'Support'
|
||||
}
|
||||
return labels[role] || role
|
||||
}
|
||||
|
||||
// templates and default_templates come directly from provider
|
||||
const templates = computed(() => provider.value?.templates ?? {})
|
||||
const defaultTemplates = computed(() => provider.value?.default_templates ?? {})
|
||||
@@ -81,7 +92,7 @@ async function toggleTemplate(templateId: string, active: boolean, policyType: s
|
||||
try {
|
||||
await $providers(`/providers/${providerId}/templates/${templateId}/${path}`, {
|
||||
method: 'POST',
|
||||
body: { policy_type: policyType, client_type: client_type }
|
||||
body: { policy_type: policyType, client_type: clientType }
|
||||
})
|
||||
toast.add({ title: `Template ${active ? 'deactivated' : 'activated'}`, color: 'green' })
|
||||
await refresh()
|
||||
@@ -182,7 +193,7 @@ const clientTypeColor = (ct: string) =>
|
||||
from these slots.
|
||||
</p>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<UFormField v-for="role in roles" :key="role" :label="label(role)">
|
||||
<UFormField v-for="role in roles" :key="role" :label="getRoleLabel(role)">
|
||||
<UInput v-model="emails[role]" type="email" placeholder="name@carrier.com" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
@@ -9,62 +9,91 @@ interface QuickLead {
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
product: string
|
||||
source: string
|
||||
priority: 'normal' | 'high' | 'urgent'
|
||||
note: string
|
||||
agent: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/* ── Storage ── */
|
||||
const STORAGE_KEY = 'policy-ui.quick-leads'
|
||||
|
||||
function loadLeads(): QuickLead[] {
|
||||
if (import.meta.client) {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) {
|
||||
return JSON.parse(raw) as QuickLead[]
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function saveLeads(leads: QuickLead[]) {
|
||||
if (import.meta.client) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(leads))
|
||||
} catch { /* quota */ }
|
||||
}
|
||||
notes: string
|
||||
priority: 'low' | 'medium' | 'high'
|
||||
source: 'website' | 'referral' | 'social_media' | 'cold_call' | 'email_campaign' | 'other'
|
||||
status: 'new' | 'contacted' | 'qualified' | 'proposal' | 'negotiation' | 'converted' | 'lost'
|
||||
assigned_to?: string
|
||||
company_name?: string
|
||||
estimated_value?: string
|
||||
expected_close_date?: string
|
||||
inserted_at: string
|
||||
updated_at: string
|
||||
status_history?: any[]
|
||||
}
|
||||
|
||||
/* ── State ── */
|
||||
const leads = ref<QuickLead[]>(loadLeads())
|
||||
const page = ref(1)
|
||||
const pageSize = ref(100)
|
||||
|
||||
watch(leads, (v) => {
|
||||
saveLeads(v)
|
||||
}, { deep: true })
|
||||
/* ── List filtering ── */
|
||||
type ListFilter = 'all' | 'high' | 'medium' | 'low'
|
||||
const activeFilter = ref<ListFilter>('all')
|
||||
|
||||
function addLead(lead: Omit<QuickLead, 'id' | 'createdAt'>) {
|
||||
const newLead: QuickLead = {
|
||||
id: crypto.randomUUID?.() ?? String(Date.now() + Math.random()),
|
||||
createdAt: new Date().toISOString(),
|
||||
...lead,
|
||||
// Fetch leads from API
|
||||
const { data: leadsData, pending: leadsPending, refresh: refreshLeads } = useCustomer('/leads', {
|
||||
query: computed(() => {
|
||||
const filters: Record<string, string> = {}
|
||||
let i = 0
|
||||
|
||||
if (activeFilter.value !== 'all') {
|
||||
filters[`filters[${i}][field]`] = 'priority'
|
||||
filters[`filters[${i}][op]`] = '=='
|
||||
filters[`filters[${i}][value]`] = activeFilter.value
|
||||
i++
|
||||
}
|
||||
|
||||
return {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
...filters
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const leads = computed(() => leadsData.value?.data ?? [])
|
||||
const meta = computed(() => leadsData.value?.meta)
|
||||
|
||||
// Create lead function
|
||||
async function createLead(leadData: Partial<QuickLead>) {
|
||||
try {
|
||||
await $fetch('/api/v1/leads', {
|
||||
baseURL: 'https://dev.api.corredorconect.com/customer',
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: leadData.name,
|
||||
phone: leadData.phone,
|
||||
email: leadData.email,
|
||||
notes: leadData.notes,
|
||||
priority: leadData.priority,
|
||||
source: leadData.source,
|
||||
status: 'new',
|
||||
assigned_to: leadData.assigned_to,
|
||||
company_name: leadData.company_name,
|
||||
estimated_value: leadData.estimated_value,
|
||||
expected_close_date: leadData.expected_close_date
|
||||
}
|
||||
})
|
||||
await refreshLeads()
|
||||
} catch (error) {
|
||||
console.error('Failed to create lead:', error)
|
||||
throw error
|
||||
}
|
||||
leads.value = [newLead, ...leads.value]
|
||||
}
|
||||
|
||||
function removeLead(id: string) {
|
||||
leads.value = leads.value.filter(l => l.id !== id)
|
||||
}
|
||||
|
||||
function recentLeads(days: number): QuickLead[] {
|
||||
const cutoff = Date.now() - days * 86400000
|
||||
return leads.value.filter(l => new Date(l.createdAt).getTime() > cutoff)
|
||||
// Update lead status function
|
||||
async function updateLeadStatus(id: string, status: QuickLead['status']) {
|
||||
try {
|
||||
await $fetch(`/api/v1/leads/${id}/status`, {
|
||||
baseURL: 'https://dev.api.corredorconect.com/customer',
|
||||
method: 'PUT',
|
||||
body: { status }
|
||||
})
|
||||
await refreshLeads()
|
||||
} catch (error) {
|
||||
console.error('Failed to update lead status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Form state ── */
|
||||
@@ -72,69 +101,109 @@ const formOpen = ref(false)
|
||||
const name = ref('')
|
||||
const phone = ref('')
|
||||
const email = ref('')
|
||||
const product = ref('')
|
||||
const notes = ref('')
|
||||
const priority = ref<'low' | 'medium' | 'high'>('low')
|
||||
const source = ref('')
|
||||
const priority = ref<'normal' | 'high' | 'urgent'>('normal')
|
||||
const note = ref('')
|
||||
|
||||
const productOptions = [
|
||||
{ label: 'Auto', value: 'Auto' },
|
||||
{ label: 'Health', value: 'Health' },
|
||||
{ label: 'Life', value: 'Life' },
|
||||
{ label: 'General Risk', value: 'General Risk' },
|
||||
{ label: 'Custom', value: 'Custom' },
|
||||
]
|
||||
const assigned_to = ref('')
|
||||
const company_name = ref('')
|
||||
const estimated_value = ref('')
|
||||
const expected_close_date = ref('')
|
||||
|
||||
const sourceOptions = [
|
||||
{ label: 'Walk-in', value: 'walk-in' },
|
||||
{ label: 'Referral', value: 'referral' },
|
||||
{ label: 'Phone call', value: 'phone' },
|
||||
{ label: 'Website', value: 'website' },
|
||||
{ label: 'Social media', value: 'social' },
|
||||
{ label: 'Referral', value: 'referral' },
|
||||
{ label: 'Social media', value: 'social_media' },
|
||||
{ label: 'Cold call', value: 'cold_call' },
|
||||
{ label: 'Email campaign', value: 'email_campaign' },
|
||||
{ label: 'Other', value: 'other' },
|
||||
]
|
||||
|
||||
const priorityOptions = [
|
||||
{ label: 'Normal', value: 'normal' as const },
|
||||
{ label: 'High — follow up today', value: 'high' as const },
|
||||
{ label: 'Urgent — client waiting', value: 'urgent' as const },
|
||||
{ label: 'Low', value: 'low' as const },
|
||||
{ label: 'Medium', value: 'medium' as const },
|
||||
{ label: 'High', value: 'high' as const },
|
||||
]
|
||||
|
||||
function submit() {
|
||||
if (!name.value.trim() || !product.value) return
|
||||
addLead({
|
||||
name: name.value.trim(),
|
||||
phone: phone.value.trim(),
|
||||
email: email.value.trim(),
|
||||
product: product.value,
|
||||
source: source.value || 'other',
|
||||
priority: priority.value,
|
||||
note: note.value.trim(),
|
||||
agent: 'Me', // placeholder
|
||||
})
|
||||
toast.add({ title: 'Lead captured', description: `${name.value} added to quick leads`, color: 'success' })
|
||||
resetForm()
|
||||
const statusOptions = [
|
||||
{ label: 'New', value: 'new' },
|
||||
{ label: 'Contacted', value: 'contacted' },
|
||||
{ label: 'Qualified', value: 'qualified' },
|
||||
{ label: 'Proposal', value: 'proposal' },
|
||||
{ label: 'Negotiation', value: 'negotiation' },
|
||||
{ label: 'Converted', value: 'converted' },
|
||||
{ label: 'Lost', value: 'lost' },
|
||||
]
|
||||
|
||||
async function submit() {
|
||||
if (!name.value.trim()) return
|
||||
|
||||
try {
|
||||
await createLead({
|
||||
name: name.value.trim(),
|
||||
phone: phone.value.trim(),
|
||||
email: email.value.trim(),
|
||||
notes: notes.value.trim(),
|
||||
priority: priority.value,
|
||||
source: source.value || 'other',
|
||||
assigned_to: assigned_to.value.trim() || undefined,
|
||||
company_name: company_name.value.trim() || undefined,
|
||||
estimated_value: estimated_value.value.trim() || undefined,
|
||||
expected_close_date: expected_close_date.value || undefined,
|
||||
})
|
||||
|
||||
toast.add({ title: 'Lead captured', description: `${name.value} added to quick leads`, color: 'success' })
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
toast.add({ title: 'Failed to create lead', description: 'Please try again', color: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
name.value = ''
|
||||
phone.value = ''
|
||||
email.value = ''
|
||||
product.value = ''
|
||||
notes.value = ''
|
||||
priority.value = 'low'
|
||||
source.value = ''
|
||||
priority.value = 'normal'
|
||||
note.value = ''
|
||||
assigned_to.value = ''
|
||||
company_name.value = ''
|
||||
estimated_value.value = ''
|
||||
expected_close_date.value = ''
|
||||
formOpen.value = false
|
||||
}
|
||||
|
||||
function confirmRemove(id: string) {
|
||||
removeLead(id)
|
||||
toast.add({ title: 'Lead removed', color: 'neutral' })
|
||||
async function handleStatusChange(leadId: string, newStatus: QuickLead['status']) {
|
||||
try {
|
||||
await updateLeadStatus(leadId, newStatus)
|
||||
toast.add({ title: 'Status updated', description: `Lead status changed to ${newStatus}`, color: 'success' })
|
||||
} catch (error) {
|
||||
toast.add({ title: 'Failed to update status', description: 'Please try again', color: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
/* ── List filtering ── */
|
||||
type ListFilter = 'all' | 'urgent' | 'high' | 'normal'
|
||||
const activeFilter = ref<ListFilter>('all')
|
||||
/* ── Drag and drop for status ── */
|
||||
const draggedLead = ref<string | null>(null)
|
||||
const draggedOverStatus = ref<QuickLead['status'] | null>(null)
|
||||
|
||||
function handleDragStart(leadId: string) {
|
||||
draggedLead.value = leadId
|
||||
}
|
||||
|
||||
function handleDragOver(status: QuickLead['status']) {
|
||||
draggedOverStatus.value = status
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
if (draggedLead.value && draggedOverStatus.value) {
|
||||
handleStatusChange(draggedLead.value, draggedOverStatus.value)
|
||||
}
|
||||
draggedLead.value = null
|
||||
draggedOverStatus.value = null
|
||||
}
|
||||
|
||||
function handleDrop(leadId: string, newStatus: QuickLead['status']) {
|
||||
handleStatusChange(leadId, newStatus)
|
||||
}
|
||||
|
||||
const filteredLeads = computed(() => {
|
||||
if (activeFilter.value === 'all') return leads.value
|
||||
@@ -143,16 +212,34 @@ const filteredLeads = computed(() => {
|
||||
|
||||
const filterCounts = computed(() => ({
|
||||
all: leads.value.length,
|
||||
urgent: leads.value.filter(l => l.priority === 'urgent').length,
|
||||
high: leads.value.filter(l => l.priority === 'high').length,
|
||||
normal: leads.value.filter(l => l.priority === 'normal').length,
|
||||
medium: leads.value.filter(l => l.priority === 'medium').length,
|
||||
low: leads.value.filter(l => l.priority === 'low').length,
|
||||
}))
|
||||
|
||||
const recentLeads = computed(() => {
|
||||
const cutoff = Date.now() - 10 * 86400000
|
||||
return leads.value.filter(l => new Date(l.inserted_at).getTime() > cutoff)
|
||||
})
|
||||
|
||||
/* ── Helpers ── */
|
||||
function priorityMeta(p: string) {
|
||||
if (p === 'urgent') return { label: 'Urgent', class: 'ql-pri-urgent' }
|
||||
if (p === 'high') return { label: 'High', class: 'ql-pri-high' }
|
||||
return { label: 'Normal', class: 'ql-pri-normal' }
|
||||
if (p === 'high') return { label: 'High', class: 'ql-pri-urgent' }
|
||||
if (p === 'medium') return { label: 'Medium', class: 'ql-pri-high' }
|
||||
return { label: 'Low', class: 'ql-pri-normal' }
|
||||
}
|
||||
|
||||
function statusMeta(s: string) {
|
||||
const statusMap: Record<string, { label: string; class: string }> = {
|
||||
new: { label: 'New', class: 'ql-status-new' },
|
||||
contacted: { label: 'Contacted', class: 'ql-status-contacted' },
|
||||
qualified: { label: 'Qualified', class: 'ql-status-qualified' },
|
||||
proposal: { label: 'Proposal', class: 'ql-status-proposal' },
|
||||
negotiation: { label: 'Negotiation', class: 'ql-status-negotiation' },
|
||||
converted: { label: 'Converted', class: 'ql-status-converted' },
|
||||
lost: { label: 'Lost', class: 'ql-status-lost' },
|
||||
}
|
||||
return statusMap[s] || { label: s, class: 'ql-status-new' }
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
@@ -228,21 +315,33 @@ function formatDate(iso: string) {
|
||||
<div class="ql-section">
|
||||
<p class="ql-section-title">Lead info</p>
|
||||
<div class="ql-fields">
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Product line <span class="ql-required">*</span></label>
|
||||
<USelect v-model="product" :items="productOptions" placeholder="Select line..." size="sm" />
|
||||
</div>
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Source</label>
|
||||
<USelect v-model="source" :items="sourceOptions" placeholder="How did they find us?" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field ql-field-full">
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Priority</label>
|
||||
<USelect v-model="priority" :items="priorityOptions" placeholder="Normal" size="sm" />
|
||||
<USelect v-model="priority" :items="priorityOptions" placeholder="Low" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Assigned to</label>
|
||||
<UInput v-model="assigned_to" placeholder="Agent name" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Company name</label>
|
||||
<UInput v-model="company_name" placeholder="Company (optional)" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Estimated value</label>
|
||||
<UInput v-model="estimated_value" placeholder="$0.00" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field">
|
||||
<label class="ql-label">Expected close date</label>
|
||||
<UInput v-model="expected_close_date" type="date" size="sm" />
|
||||
</div>
|
||||
<div class="ql-field ql-field-full">
|
||||
<label class="ql-label">Notes</label>
|
||||
<UTextarea v-model="note" placeholder="Brief context, referral source, or anything useful for follow-up..." size="sm" :rows="2" />
|
||||
<UTextarea v-model="notes" placeholder="Brief context, referral source, or anything useful for follow-up..." size="sm" :rows="2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +354,7 @@ function formatDate(iso: string) {
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="ql-cancel-btn" @click="resetForm">Cancel</button>
|
||||
<button type="button" class="ql-submit-btn" :class="!name.trim() || !product ? 'ql-btn-disabled' : ''" @click="submit">
|
||||
<button type="button" class="ql-submit-btn" :class="!name.trim() ? 'ql-btn-disabled' : ''" @click="submit">
|
||||
<UIcon name="i-heroicons-paper-airplane" style="width: 14px; height: 14px;" />
|
||||
Add Lead
|
||||
</button>
|
||||
@@ -272,15 +371,15 @@ function formatDate(iso: string) {
|
||||
</div>
|
||||
<div class="ql-kpi">
|
||||
<p class="ql-kpi-label">Last 10 days</p>
|
||||
<p class="ql-kpi-value">{{ recentLeads(10).length }}</p>
|
||||
</div>
|
||||
<div class="ql-kpi">
|
||||
<p class="ql-kpi-label">Urgent</p>
|
||||
<p class="ql-kpi-value" style="color: #c13838;">{{ filterCounts.urgent }}</p>
|
||||
<p class="ql-kpi-value">{{ recentLeads.length }}</p>
|
||||
</div>
|
||||
<div class="ql-kpi">
|
||||
<p class="ql-kpi-label">High priority</p>
|
||||
<p class="ql-kpi-value" style="color: #c27b1a;">{{ filterCounts.high }}</p>
|
||||
<p class="ql-kpi-value" style="color: #c13838;">{{ filterCounts.high }}</p>
|
||||
</div>
|
||||
<div class="ql-kpi">
|
||||
<p class="ql-kpi-label">Medium priority</p>
|
||||
<p class="ql-kpi-value" style="color: #c27b1a;">{{ filterCounts.medium }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,9 +389,9 @@ function formatDate(iso: string) {
|
||||
<button
|
||||
v-for="f in ([
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'urgent', label: 'Urgent' },
|
||||
{ id: 'high', label: 'High' },
|
||||
{ id: 'normal', label: 'Normal' },
|
||||
{ id: 'medium', label: 'Medium' },
|
||||
{ id: 'low', label: 'Low' },
|
||||
] as { id: ListFilter; label: string }[])"
|
||||
:key="f.id"
|
||||
type="button"
|
||||
@@ -308,7 +407,12 @@ function formatDate(iso: string) {
|
||||
</div>
|
||||
|
||||
<!-- ═══ Leads list ═══ -->
|
||||
<div v-if="filteredLeads.length === 0" class="ql-empty">
|
||||
<div v-if="leadsPending" class="ql-empty">
|
||||
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin" style="color: #c0c0bc;" />
|
||||
<p class="text-[13px] text-[var(--text-muted)] mt-2">Loading leads...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredLeads.length === 0" class="ql-empty">
|
||||
<UIcon name="i-heroicons-bolt" style="width: 32px; height: 32px; color: #c0c0bc;" />
|
||||
<p class="text-[13px] text-[var(--text-muted)] mt-2">No quick leads yet.</p>
|
||||
<button type="button" class="ql-add-btn mt-3" @click="formOpen = true">
|
||||
@@ -333,32 +437,47 @@ function formatDate(iso: string) {
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<p class="text-[14px] font-semibold text-[var(--text-primary)] truncate">{{ lead.name }}</p>
|
||||
<span :class="priorityMeta(lead.priority).class">{{ priorityMeta(lead.priority).label }}</span>
|
||||
<span class="ql-product-tag">{{ lead.product }}</span>
|
||||
<div
|
||||
:class="statusMeta(lead.status).class"
|
||||
class="cursor-pointer hover:opacity-80"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart(lead.id)"
|
||||
@dragend="handleDragEnd"
|
||||
@dragover.prevent="handleDragOver(lead.status)"
|
||||
@drop="handleDrop(lead.id, lead.status)"
|
||||
title="Drag to change status"
|
||||
>
|
||||
{{ statusMeta(lead.status).label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--text-muted)]">
|
||||
<span v-if="lead.phone">{{ lead.phone }}</span>
|
||||
<span v-if="lead.email">{{ lead.email }}</span>
|
||||
<span>{{ formatDate(lead.createdAt) }}</span>
|
||||
<span>{{ formatDate(lead.inserted_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ql-lead-actions">
|
||||
<UDropdown :items="statusOptions.map(s => ({ label: s.label, click: () => handleStatusChange(lead.id, s.value as QuickLead['status']) }))">
|
||||
<button type="button" class="ql-action-btn" title="Change status">
|
||||
<UIcon name="i-heroicons-arrows-pointing-out" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</UDropdown>
|
||||
<NuxtLink :to="`/quotes/new`" title="Start quote">
|
||||
<button type="button" class="ql-action-btn ql-action-quote">
|
||||
<UIcon name="i-heroicons-calculator" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</NuxtLink>
|
||||
<button type="button" class="ql-action-btn ql-action-delete" title="Remove" @click="confirmRemove(lead.id)">
|
||||
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="lead.note" class="ql-lead-note">
|
||||
<div v-if="lead.notes" class="ql-lead-note">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-ellipsis" style="width: 11px; height: 11px; color: #8a8a86; flex-shrink: 0;" />
|
||||
<span>{{ lead.note }}</span>
|
||||
<span>{{ lead.notes }}</span>
|
||||
</div>
|
||||
<div class="ql-lead-meta">
|
||||
<span v-if="lead.source" class="ql-meta-tag">{{ lead.source }}</span>
|
||||
<span class="ql-meta-tag">{{ lead.agent }}</span>
|
||||
<span v-if="lead.company_name" class="ql-meta-tag">{{ lead.company_name }}</span>
|
||||
<span v-if="lead.assigned_to" class="ql-meta-tag">{{ lead.assigned_to }}</span>
|
||||
<span v-if="lead.estimated_value" class="ql-meta-tag">{{ lead.estimated_value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
@@ -546,9 +665,35 @@ function formatDate(iso: string) {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap;
|
||||
}
|
||||
.ql-product-tag {
|
||||
|
||||
/* ── Status badges ── */
|
||||
.ql-status-new {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(1,105,111,0.07); color: #01696f; white-space: nowrap;
|
||||
background: rgba(1,105,111,0.08); color: #01696f; white-space: nowrap;
|
||||
}
|
||||
.ql-status-contacted {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(59,130,246,0.08); color: #3b82f6; white-space: nowrap;
|
||||
}
|
||||
.ql-status-qualified {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(16,185,129,0.08); color: #10b981; white-space: nowrap;
|
||||
}
|
||||
.ql-status-proposal {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(245,158,11,0.08); color: #f59e0b; white-space: nowrap;
|
||||
}
|
||||
.ql-status-negotiation {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(139,92,246,0.08); color: #8b5cf6; white-space: nowrap;
|
||||
}
|
||||
.ql-status-converted {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(34,197,94,0.08); color: #22c55e; white-space: nowrap;
|
||||
}
|
||||
.ql-status-lost {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(107,114,128,0.08); color: #6b7280; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Empty ── */
|
||||
|
||||
@@ -18,5 +18,39 @@
|
||||
},
|
||||
"plugin": [
|
||||
"opencode-browser"
|
||||
]
|
||||
],
|
||||
"agent": {
|
||||
"bug-finder": {
|
||||
"description": "Systematically tests all UI pages using webmcp and documents bugs in BUGS.md",
|
||||
"model": "z-ai/glm4.7",
|
||||
"prompt": "You are a UI bug finder agent. Your task is to systematically test all pages in the application using browsermcp tools and document any bugs found in BUGS.md.\n\nTesting Methodology:\n1. Start at http://localhost:3000/\n2. Navigate through all pages systematically\n3. For each page, test: page loads without errors, data displays correctly, forms validate properly, interactive elements work, responsive layout, page refresh behavior, navigation links work, no console errors\n4. Document bugs with priority classification (Critical, High, Medium, Low)\n5. Use the exact format from existing BUGS.md for consistency\n\nPages to test (in order):\n- /, /customers, /customers/new, /customers/[id]\n- /policies, /policies/new, /policies/[id], /policies/book, /policies/groups\n- /quotes/new (test all tabs: car, life, fire_structure, fire_contents)\n- /providers, /providers/new, /providers/[provider_id]\n- /settings and all subpages\n- /workstation pages (customer-service, renewals, facturacion, collectivos, claims, collections)\n- /calendar, /collections, /analysis, /account\n- /ai-tools pages (sales-factory, policy-comparator, email-writer, case-assistant)\n- /back-office/workload, /back-office/workload/[id], /back-office/workload/kanban\n- /sales/leads, /sales/quick-lead\n\nBug Documentation Format:\n### [Priority] Bug Title\n- **Issue**: Clear description\n- **Location**: Page/route\n- **Impact**: User experience severity \n- **Status**: Pending\n- **Test Results**: Specific observations\n\nTrack visited URLs and update BUGS.md with findings.",
|
||||
"tools": {
|
||||
"browsermcp_browser_navigate": true,
|
||||
"browsermcp_browser_snapshot": true,
|
||||
"browsermcp_browser_click": true,
|
||||
"browsermcp_browser_type": true,
|
||||
"browsermcp_browser_select_option": true,
|
||||
"browsermcp_browser_screenshot": true,
|
||||
"browsermcp_browser_get_console_logs": true,
|
||||
"read": true,
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"glob": true,
|
||||
"grep": true
|
||||
}
|
||||
},
|
||||
"bug-fixer": {
|
||||
"description": "Reads BUGS.md and systematically fixes all documented bugs",
|
||||
"model": "z-ai/glm4.7",
|
||||
"prompt": "You are a bug fixer agent. Your task is to read BUGS.md and systematically fix all documented bugs.\n\nFix Process:\n1. Read BUGS.md to identify all pending bugs\n2. Categorize bugs by type (routing, validation, layout, data, UI)\n3. Fix bugs in severity order: Critical → High → Medium → Low\n4. For each bug:\n - Locate affected files using glob and grep\n - Understand root cause by reading relevant code\n - Implement fix following existing patterns and conventions\n - Update bug status in BUGS.md with ✅ FIXED\n - Add notes about what was fixed\n\nCode Quality Guidelines:\n- Follow existing code conventions and patterns\n- Use existing libraries and utilities\n- Maintain consistency with similar fixes\n- No comments unless requested\n- Test fixes if possible\n\nAfter fixing all bugs, provide summary of fixes made.",
|
||||
"tools": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"edit": true,
|
||||
"glob": true,
|
||||
"grep": true,
|
||||
"bash": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user