Files
policy-ui/app/pages/sales/quick-lead.vue
HaimKortovich f19a727ef0
All checks were successful
Build and Publish / build-release (push) Successful in 4m11s
add quick leads
2026-05-04 13:06:09 -05:00

705 lines
25 KiB
Vue

<script setup lang="ts">
usePageTitle('Quick Leads')
const toast = useToast()
/* ── Types ── */
interface QuickLead {
id: string
name: string
phone: string
email: string
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 page = ref(1)
const pageSize = ref(100)
/* ── List filtering ── */
type ListFilter = 'all' | 'high' | 'medium' | 'low'
const activeFilter = ref<ListFilter>('all')
// 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
}
}
// 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 ── */
const formOpen = ref(false)
const name = ref('')
const phone = ref('')
const email = ref('')
const notes = ref('')
const priority = ref<'low' | 'medium' | 'high'>('low')
const source = ref('')
const assigned_to = ref('')
const company_name = ref('')
const estimated_value = ref('')
const expected_close_date = ref('')
const sourceOptions = [
{ label: 'Website', value: 'website' },
{ 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: 'Low', value: 'low' as const },
{ label: 'Medium', value: 'medium' as const },
{ label: 'High', value: 'high' as const },
]
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 = ''
notes.value = ''
priority.value = 'low'
source.value = ''
assigned_to.value = ''
company_name.value = ''
estimated_value.value = ''
expected_close_date.value = ''
formOpen.value = false
}
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' })
}
}
/* ── 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
return leads.value.filter(l => l.priority === activeFilter.value)
})
const filterCounts = computed(() => ({
all: leads.value.length,
high: leads.value.filter(l => l.priority === 'high').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 === '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) {
const d = new Date(iso)
const now = new Date()
const diff = now.getTime() - d.getTime()
if (diff < 3600000) return `${Math.max(1, Math.round(diff / 60000))}m ago`
if (diff < 86400000) return `${Math.round(diff / 3600000)}h ago`
if (diff < 172800000) return 'Yesterday'
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
</script>
<template>
<div class="ql-page">
<!-- Back -->
<NuxtLink to="/onboarding" class="inline-flex">
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Sales Pipeline</UButton>
</NuxtLink>
<!-- Sales flow indicator -->
<SalesFlowIndicator current-stage="quick_lead" />
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Quick Leads</h1>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
Capture leads in seconds. Every lead lands here and stays visible until you move it to a full quote or customer profile.
</p>
</div>
<button
type="button"
class="ql-add-btn"
@click="formOpen = !formOpen"
>
<UIcon :name="formOpen ? 'i-heroicons-chevron-up' : 'i-heroicons-plus'" style="width: 14px; height: 14px;" />
{{ formOpen ? 'Close form' : 'New quick lead' }}
</button>
</div>
<!-- Dropdown form -->
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2 max-h-0"
enter-to-class="opacity-100 translate-y-0 max-h-[600px]"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 max-h-[600px]"
leave-to-class="opacity-0 -translate-y-2 max-h-0"
>
<div v-if="formOpen" class="ql-form-card">
<!-- Section: Contact -->
<div class="ql-section">
<p class="ql-section-title">Contact</p>
<div class="ql-fields">
<div class="ql-field ql-field-full">
<label class="ql-label">Name <span class="ql-required">*</span></label>
<UInput v-model="name" placeholder="Full name or company" size="sm" />
</div>
<div class="ql-field">
<label class="ql-label">Phone</label>
<UInput v-model="phone" placeholder="+506 0000-0000" size="sm" />
</div>
<div class="ql-field">
<label class="ql-label">Email</label>
<UInput v-model="email" placeholder="name@company.com" size="sm" type="email" />
</div>
</div>
</div>
<div class="ql-divider" />
<!-- Section: Lead info -->
<div class="ql-section">
<p class="ql-section-title">Lead info</p>
<div class="ql-fields">
<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">
<label class="ql-label">Priority</label>
<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="notes" placeholder="Brief context, referral source, or anything useful for follow-up..." size="sm" :rows="2" />
</div>
</div>
</div>
<!-- Submit -->
<div class="ql-footer">
<div class="flex items-center gap-2 text-[12px] text-[var(--text-muted)]">
<UIcon name="i-heroicons-bolt" style="width: 14px; height: 14px; opacity: 0.5;" />
Saves to quick lead list
</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() ? 'ql-btn-disabled' : ''" @click="submit">
<UIcon name="i-heroicons-paper-airplane" style="width: 14px; height: 14px;" />
Add Lead
</button>
</div>
</div>
</div>
</Transition>
<!-- KPI strip -->
<div class="ql-kpi-strip">
<div class="ql-kpi">
<p class="ql-kpi-label">Total leads</p>
<p class="ql-kpi-value">{{ leads.length }}</p>
</div>
<div class="ql-kpi">
<p class="ql-kpi-label">Last 10 days</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: #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>
<!-- Filter tabs -->
<div class="flex items-center justify-between gap-3">
<div class="ql-filter-tabs">
<button
v-for="f in ([
{ id: 'all', label: 'All' },
{ id: 'high', label: 'High' },
{ id: 'medium', label: 'Medium' },
{ id: 'low', label: 'Low' },
] as { id: ListFilter; label: string }[])"
:key="f.id"
type="button"
class="ql-filter-tab"
:class="activeFilter === f.id ? 'ql-filter-on' : 'ql-filter-off'"
@click="activeFilter = f.id"
>
{{ f.label }}
<span class="ql-filter-count" :class="activeFilter === f.id ? 'ql-filter-count-on' : ''">{{ filterCounts[f.id] }}</span>
</button>
</div>
<span class="text-[11px] text-[var(--text-muted)]">{{ filteredLeads.length }} results</span>
</div>
<!-- ═══ Leads list ═══ -->
<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">
<UIcon name="i-heroicons-plus" style="width: 14px; height: 14px;" />
Add your first lead
</button>
</div>
<div v-else class="ql-lead-list">
<TransitionGroup
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-1 scale-95"
enter-to-class="opacity-100 translate-y-0 scale-100"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0 scale-95"
>
<div v-for="lead in filteredLeads" :key="lead.id" class="ql-lead-card group">
<div class="ql-lead-top">
<div class="ql-lead-avatar">{{ lead.name.split(' ').map(w => w[0]).join('').slice(0, 2) }}</div>
<div class="min-w-0 flex-1">
<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>
<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.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>
</div>
</div>
<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.notes }}</span>
</div>
<div class="ql-lead-meta">
<span v-if="lead.source" class="ql-meta-tag">{{ lead.source }}</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>
</div>
</div>
</template>
<style scoped>
.ql-page {
max-width: 64rem;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 24px;
padding-bottom: 3rem;
}
/* ── Add button ── */
.ql-add-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
background: #01696f;
color: #fff;
font-size: 13px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 150ms ease;
white-space: nowrap;
}
.ql-add-btn:hover { background: #015458; }
/* ── Form card ── */
.ql-form-card {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
overflow: hidden;
}
.ql-section { padding: 20px; }
.ql-section-title {
font-size: 13px; font-weight: 600;
color: var(--text-primary); margin-bottom: 16px;
}
.ql-fields {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;
}
@media (max-width: 639px) { .ql-fields { grid-template-columns: 1fr; } }
.ql-field-full { grid-column: 1 / -1; }
.ql-field { display: flex; flex-direction: column; gap: 6px; }
.ql-label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: #8a8a86;
}
.ql-required { color: #c13838; }
.ql-divider { height: 1px; background: rgba(0, 0, 0, 0.06); margin: 0 20px; }
.ql-footer {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 16px 20px; border-top: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.015);
}
.ql-submit-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 16px; border-radius: 8px;
background: #01696f; color: #fff;
font-size: 13px; font-weight: 500; border: none;
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.ql-submit-btn:hover { background: #015458; }
.ql-btn-disabled { opacity: 0.5; pointer-events: none; }
.ql-cancel-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 8px 14px; border-radius: 8px;
background: transparent; color: var(--text-muted);
font-size: 13px; font-weight: 500;
border: 1px solid rgba(0,0,0,0.08); cursor: pointer;
transition: all 150ms ease; white-space: nowrap;
}
.ql-cancel-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
/* ── KPI strip ── */
.ql-kpi-strip {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
background: rgba(0,0,0,0.06); box-shadow: 0 1px 3px rgba(0,0,0,0.03);
overflow: hidden;
}
.ql-kpi { padding: 14px 18px; background: #fff; }
.ql-kpi:first-child { border-radius: 12px 0 0 12px; }
.ql-kpi:last-child { border-radius: 0 12px 12px 0; }
.ql-kpi-label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.04em; color: #8a8a86;
}
.ql-kpi-value {
margin-top: 4px; font-size: 22px; font-weight: 600;
color: var(--text-primary); font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) { .ql-kpi-strip { grid-template-columns: repeat(2, 1fr); } }
/* ── Filter tabs ── */
.ql-filter-tabs {
display: inline-flex; gap: 2px; padding: 3px;
border-radius: 10px; background: rgba(0,0,0,0.04);
}
.ql-filter-tab {
display: inline-flex; align-items: center; gap: 5px;
padding: 6px 12px; border-radius: 8px;
font-size: 12px; font-weight: 500; border: none;
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.ql-filter-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.ql-filter-off { background: transparent; color: var(--text-muted); }
.ql-filter-off:hover { color: var(--text-primary); }
.ql-filter-count {
font-size: 10px; font-weight: 600; padding: 1px 5px;
border-radius: 9999px; background: rgba(0,0,0,0.06); color: var(--text-muted);
}
.ql-filter-count-on { background: rgba(1,105,111,0.1); color: #01696f; }
/* ── Lead cards ── */
.ql-lead-list { display: flex; flex-direction: column; gap: 6px; }
.ql-lead-card {
display: flex; flex-direction: column; gap: 8px;
padding: 14px 16px; border-radius: 10px;
border: 1px solid rgba(0,0,0,0.06); background: #fff;
transition: all 150ms ease;
}
.ql-lead-card:hover {
border-color: rgba(1,105,111,0.15);
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.ql-lead-top { display: flex; align-items: center; gap: 10px; }
.ql-lead-avatar {
width: 36px; height: 36px; border-radius: 10px;
background: rgba(194,123,26,0.08); color: #c27b1a;
font-size: 12px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.ql-lead-actions {
display: flex; gap: 4px; opacity: 0;
transition: opacity 150ms ease; flex-shrink: 0;
}
.ql-lead-card:hover .ql-lead-actions { opacity: 1; }
.ql-action-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 6px;
border: none; cursor: pointer;
background: rgba(0,0,0,0.03); color: #8a8a86;
transition: all 150ms ease;
}
.ql-action-btn:hover { background: rgba(0,0,0,0.06); color: var(--text-primary); }
.ql-action-quote:hover { background: rgba(1,105,111,0.1); color: #01696f; }
.ql-action-delete:hover { background: rgba(193,56,56,0.08); color: #c13838; }
.ql-lead-note {
display: flex; align-items: flex-start; gap: 5px;
padding-left: 46px;
font-size: 12px; color: var(--text-muted); line-height: 1.4;
}
.ql-lead-meta {
display: flex; gap: 4px; padding-left: 46px; flex-wrap: wrap;
}
.ql-meta-tag {
font-size: 10px; font-weight: 500; padding: 1px 7px;
border-radius: 4px; background: rgba(0,0,0,0.04); color: #8a8a86;
text-transform: capitalize;
}
/* ── Priority badges ── */
.ql-pri-urgent {
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
background: rgba(193,56,56,0.08); color: #c13838; white-space: nowrap;
}
.ql-pri-high {
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap;
}
.ql-pri-normal {
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap;
}
/* ── Status badges ── */
.ql-status-new {
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
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 ── */
.ql-empty {
display: flex; flex-direction: column; align-items: center;
padding: 40px 16px; text-align: center;
}
</style>