WIP jordan
This commit is contained in:
770
app/pages/sales/leads/index.vue
Normal file
770
app/pages/sales/leads/index.vue
Normal file
@@ -0,0 +1,770 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Leads Hub')
|
||||
|
||||
const sidebarFeatures = useSidebarFeatures()
|
||||
|
||||
/* ── Types ── */
|
||||
|
||||
type LeadSource = 'walk_in' | 'instagram' | 'facebook' | 'google' | 'referral' | 'website' | 'phone' | 'campaign'
|
||||
type LeadStatus = 'new' | 'contacted' | 'qualified' | 'proposal' | 'won' | 'lost'
|
||||
|
||||
type Lead = {
|
||||
id: string
|
||||
name: string
|
||||
email?: string
|
||||
phone?: string
|
||||
source: LeadSource
|
||||
campaignName?: string
|
||||
lob: string
|
||||
status: LeadStatus
|
||||
assignedTo: string | null
|
||||
createdAt: string
|
||||
value?: number
|
||||
note?: string
|
||||
}
|
||||
|
||||
type LeadIntegration = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
source: LeadSource
|
||||
connected: boolean
|
||||
lastSync?: string
|
||||
leadsCount: number
|
||||
description: string
|
||||
}
|
||||
|
||||
type AutoAssignRule = {
|
||||
id: string
|
||||
source: LeadSource | 'all'
|
||||
lob: string | 'all'
|
||||
agent: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
/* ── Source metadata ── */
|
||||
|
||||
const SOURCES: { value: LeadSource | 'all'; label: string; icon: string }[] = [
|
||||
{ value: 'all', label: 'All sources', icon: 'i-heroicons-funnel' },
|
||||
{ value: 'walk_in', label: 'Walk-in', icon: 'i-heroicons-building-storefront' },
|
||||
{ value: 'instagram', label: 'Instagram', icon: 'i-heroicons-camera' },
|
||||
{ value: 'facebook', label: 'Facebook', icon: 'i-heroicons-chat-bubble-left-right' },
|
||||
{ value: 'google', label: 'Google', icon: 'i-heroicons-magnifying-glass' },
|
||||
{ value: 'referral', label: 'Referral', icon: 'i-heroicons-user-group' },
|
||||
{ value: 'website', label: 'Website', icon: 'i-heroicons-globe-alt' },
|
||||
{ value: 'phone', label: 'Phone', icon: 'i-heroicons-phone' },
|
||||
{ value: 'campaign', label: 'Campaigns', icon: 'i-heroicons-megaphone' },
|
||||
]
|
||||
|
||||
const STATUS_OPTIONS: { value: LeadStatus | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: 'All statuses' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'contacted', label: 'Contacted' },
|
||||
{ value: 'qualified', label: 'Qualified' },
|
||||
{ value: 'proposal', label: 'Proposal' },
|
||||
{ value: 'won', label: 'Won' },
|
||||
{ value: 'lost', label: 'Lost' },
|
||||
]
|
||||
|
||||
const AGENTS = ['L. Chen', 'A. Morales', 'R. Vega', 'Marco V.']
|
||||
|
||||
/* ── Mock integrations ── */
|
||||
|
||||
const integrations = ref<LeadIntegration[]>([
|
||||
{ id: 'ig', name: 'Instagram', icon: 'i-heroicons-camera', source: 'instagram', connected: false, leadsCount: 0, description: 'DMs, story replies, and lead forms from Instagram Business.' },
|
||||
{ id: 'fb', name: 'Facebook', icon: 'i-heroicons-chat-bubble-left-right', source: 'facebook', connected: false, leadsCount: 0, description: 'Lead ads, Messenger, and page contact forms.' },
|
||||
{ id: 'ga', name: 'Google Ads', icon: 'i-heroicons-magnifying-glass', source: 'google', connected: true, lastSync: '2026-04-08T10:30:00Z', leadsCount: 14, description: 'Search and display campaign lead forms.' },
|
||||
{ id: 'ws', name: 'Website Forms', icon: 'i-heroicons-globe-alt', source: 'website', connected: true, lastSync: '2026-04-08T11:00:00Z', leadsCount: 8, description: 'Contact forms from your brokerage website.' },
|
||||
{ id: 'cc', name: 'Call Center', icon: 'i-heroicons-phone-arrow-down-left', source: 'phone', connected: false, leadsCount: 0, description: 'Inbound call tracking and IVR lead capture.' },
|
||||
{ id: 'cp', name: 'Custom Campaigns', icon: 'i-heroicons-megaphone', source: 'campaign', connected: true, lastSync: '2026-04-07T16:00:00Z', leadsCount: 6, description: 'Email campaigns, events, mailers, and custom outreach.' },
|
||||
])
|
||||
|
||||
/* ── Mock leads ── */
|
||||
|
||||
const leads = ref<Lead[]>([
|
||||
{ id: 'sl-1', name: 'Ana García', email: 'ana.g@mail.com', phone: '+506 7012-3344', source: 'instagram', lob: 'Auto', status: 'new', assignedTo: null, createdAt: '2026-04-08T14:30:00Z', value: 2400, note: 'DM about fleet coverage' },
|
||||
{ id: 'sl-2', name: 'Marco Rodríguez', phone: '+506 8876-5500', source: 'walk_in', lob: 'Health', status: 'contacted', assignedTo: 'A. Morales', createdAt: '2026-04-08T09:15:00Z', value: 4800, note: 'Family plan, 4 members' },
|
||||
{ id: 'sl-3', name: 'Construcciones TRC', email: 'info@trc.cr', source: 'google', lob: 'General risk', status: 'qualified', assignedTo: 'R. Vega', createdAt: '2026-04-07T16:00:00Z', value: 18500, campaignName: 'Google Ads — Commercial Q2' },
|
||||
{ id: 'sl-4', name: 'Isabel Mora', email: 'isa.mora@outlook.com', source: 'facebook', lob: 'Life', status: 'new', assignedTo: null, createdAt: '2026-04-07T11:20:00Z', value: 3200, note: 'Responded to FB carousel ad' },
|
||||
{ id: 'sl-5', name: 'Hotel Paraíso', phone: '+506 2234-8800', source: 'referral', lob: 'General risk', status: 'proposal', assignedTo: 'A. Morales', createdAt: '2026-04-06T10:00:00Z', value: 12000, note: 'Referred by Transportes Delta' },
|
||||
{ id: 'sl-6', name: 'Laura Jiménez', email: 'laura.j@gmail.com', source: 'website', lob: 'Auto', status: 'contacted', assignedTo: 'R. Vega', createdAt: '2026-04-06T08:45:00Z', value: 1800, note: 'Online form — comparativo request' },
|
||||
{ id: 'sl-7', name: 'Farmacia Salud Plus', phone: '+506 2456-7890', source: 'campaign', campaignName: 'Spring Health Push', lob: 'Health', status: 'qualified', assignedTo: 'A. Morales', createdAt: '2026-04-05T15:30:00Z', value: 9600 },
|
||||
{ id: 'sl-8', name: 'Diego Araya', source: 'phone', phone: '+506 6100-9988', lob: 'Auto', status: 'new', assignedTo: null, createdAt: '2026-04-05T12:10:00Z', value: 1500 },
|
||||
{ id: 'sl-9', name: 'Retail Express', email: 'ventas@retailexp.cr', source: 'campaign', campaignName: 'Commercial Outreach Mar', lob: 'General risk', status: 'won', assignedTo: 'R. Vega', createdAt: '2026-04-04T09:00:00Z', value: 7200 },
|
||||
{ id: 'sl-10', name: 'Sofía Vargas', source: 'instagram', phone: '+506 8300-1122', lob: 'Life', status: 'contacted', assignedTo: 'L. Chen', createdAt: '2026-04-03T17:00:00Z', value: 2800, note: 'IG story reply, wants term quote' },
|
||||
{ id: 'sl-11', name: 'Carlos Méndez', email: 'carlos.m@corp.cr', source: 'google', lob: 'Auto', status: 'new', assignedTo: null, createdAt: '2026-04-08T08:00:00Z', value: 3400 },
|
||||
{ id: 'sl-12', name: 'Inversiones Pacífico', phone: '+506 2200-4455', source: 'campaign', campaignName: 'Spring Health Push', lob: 'Health', status: 'contacted', assignedTo: 'L. Chen', createdAt: '2026-04-07T14:00:00Z', value: 15200 },
|
||||
])
|
||||
|
||||
/* ── Auto-assign rules (management) ── */
|
||||
|
||||
const autoAssignRules = ref<AutoAssignRule[]>([
|
||||
{ id: 'r1', source: 'google', lob: 'all', agent: 'R. Vega', enabled: true },
|
||||
{ id: 'r2', source: 'instagram', lob: 'all', agent: 'L. Chen', enabled: true },
|
||||
{ id: 'r3', source: 'all', lob: 'Health', agent: 'A. Morales', enabled: false },
|
||||
])
|
||||
|
||||
/* ── Filters ── */
|
||||
|
||||
const sourceFilter = ref<LeadSource | 'all'>('all')
|
||||
const statusFilter = ref<LeadStatus | 'all'>('all')
|
||||
const assignmentFilter = ref<'all' | 'assigned' | 'unassigned'>('all')
|
||||
const search = ref('')
|
||||
const activeTab = ref<'leads' | 'integrations' | 'management'>('leads')
|
||||
const releaseMode = ref<'auto_assign' | 'reception' | 'auto_release' | 'round_robin'>('auto_assign')
|
||||
|
||||
const filtered = computed(() => {
|
||||
let list = [...leads.value]
|
||||
if (sourceFilter.value !== 'all') list = list.filter(l => l.source === sourceFilter.value)
|
||||
if (statusFilter.value !== 'all') list = list.filter(l => l.status === statusFilter.value)
|
||||
if (assignmentFilter.value === 'assigned') list = list.filter(l => l.assignedTo !== null)
|
||||
if (assignmentFilter.value === 'unassigned') list = list.filter(l => l.assignedTo === null)
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (q) list = list.filter(l => l.name.toLowerCase().includes(q) || l.email?.toLowerCase().includes(q) || l.phone?.includes(q))
|
||||
return list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
})
|
||||
|
||||
const sourceCounts = computed(() => {
|
||||
const c: Record<string, number> = { all: leads.value.length }
|
||||
for (const l of leads.value) c[l.source] = (c[l.source] || 0) + 1
|
||||
return c
|
||||
})
|
||||
|
||||
const unassignedCount = computed(() => leads.value.filter(l => l.assignedTo === null).length)
|
||||
const newCount = computed(() => leads.value.filter(l => l.status === 'new').length)
|
||||
const totalPipeline = computed(() => leads.value.reduce((s, l) => s + (l.value || 0), 0))
|
||||
const connectedCount = computed(() => integrations.value.filter(i => i.connected).length)
|
||||
|
||||
/* ── Helpers ── */
|
||||
|
||||
function sourceLabel(s: LeadSource) {
|
||||
return SOURCES.find(x => x.value === s)?.label ?? s
|
||||
}
|
||||
function sourceIcon(s: LeadSource) {
|
||||
return SOURCES.find(x => x.value === s)?.icon ?? 'i-heroicons-question-mark-circle'
|
||||
}
|
||||
|
||||
function statusBadge(s: LeadStatus) {
|
||||
switch (s) {
|
||||
case 'new': return { label: 'New', cls: 'ld-status-new' }
|
||||
case 'contacted': return { label: 'Contacted', cls: 'ld-status-contacted' }
|
||||
case 'qualified': return { label: 'Qualified', cls: 'ld-status-qualified' }
|
||||
case 'proposal': return { label: 'Proposal', cls: 'ld-status-proposal' }
|
||||
case 'won': return { label: 'Won', cls: 'ld-status-won' }
|
||||
case 'lost': return { label: 'Lost', cls: 'ld-status-lost' }
|
||||
}
|
||||
}
|
||||
|
||||
function fmtValue(v?: number) {
|
||||
if (!v) return ''
|
||||
return '$' + v.toLocaleString('en-US', { minimumFractionDigits: 0 })
|
||||
}
|
||||
|
||||
function timeAgo(iso: string) {
|
||||
const diff = Date.now() - new Date(iso).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 new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
function assignLead(lead: Lead, agent: string) {
|
||||
lead.assignedTo = agent
|
||||
toast.add({ title: `${lead.name} assigned to ${agent}`, color: 'success' })
|
||||
}
|
||||
|
||||
function unassignLead(lead: Lead) {
|
||||
lead.assignedTo = null
|
||||
toast.add({ title: `${lead.name} unassigned`, color: 'neutral' })
|
||||
}
|
||||
|
||||
function toggleIntegration(intg: LeadIntegration) {
|
||||
intg.connected = !intg.connected
|
||||
if (intg.connected) {
|
||||
intg.lastSync = new Date().toISOString()
|
||||
toast.add({ title: `${intg.name} connected`, color: 'success' })
|
||||
} else {
|
||||
toast.add({ title: `${intg.name} disconnected`, color: 'neutral' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ld-page">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div class="max-w-xl">
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Leads Hub</h1>
|
||||
<p class="mt-1 text-[13px] text-[var(--text-muted)]">All leads from every source — assign, track, and connect APIs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="ld-kpi-strip">
|
||||
<div class="ld-kpi">
|
||||
<div class="ld-kpi-icon" style="background: rgba(59,130,246,0.08); color: #3b82f6">
|
||||
<UIcon name="i-heroicons-inbox-arrow-down" style="width: 15px; height: 15px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="ld-kpi-label">Total leads</p>
|
||||
<p class="ld-kpi-value">{{ leads.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ld-kpi">
|
||||
<div class="ld-kpi-icon" style="background: rgba(194,123,26,0.08); color: #c27b1a">
|
||||
<UIcon name="i-heroicons-exclamation-circle" style="width: 15px; height: 15px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="ld-kpi-label">Unassigned</p>
|
||||
<p class="ld-kpi-value">{{ unassignedCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ld-kpi">
|
||||
<div class="ld-kpi-icon" style="background: rgba(15,123,95,0.08); color: #0f7b5f">
|
||||
<UIcon name="i-heroicons-bolt" style="width: 15px; height: 15px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="ld-kpi-label">New today</p>
|
||||
<p class="ld-kpi-value">{{ newCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ld-kpi">
|
||||
<div class="ld-kpi-icon" style="background: rgba(1,105,111,0.08); color: #01696f">
|
||||
<UIcon name="i-heroicons-currency-dollar" style="width: 15px; height: 15px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="ld-kpi-label">Pipeline value</p>
|
||||
<p class="ld-kpi-value">{{ fmtValue(totalPipeline) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ld-kpi">
|
||||
<div class="ld-kpi-icon" style="background: rgba(139,92,246,0.08); color: #8b5cf6">
|
||||
<UIcon name="i-heroicons-signal" style="width: 15px; height: 15px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="ld-kpi-label">Connected</p>
|
||||
<p class="ld-kpi-value">{{ connectedCount }}/{{ integrations.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab navigation -->
|
||||
<div class="ld-tabs">
|
||||
<button :class="['ld-tab', activeTab === 'leads' ? 'ld-tab--active' : '']" @click="activeTab = 'leads'">
|
||||
<UIcon name="i-heroicons-user-group" style="width: 14px; height: 14px;" />
|
||||
Leads
|
||||
</button>
|
||||
<button :class="['ld-tab', activeTab === 'integrations' ? 'ld-tab--active' : '']" @click="activeTab = 'integrations'">
|
||||
<UIcon name="i-heroicons-signal" style="width: 14px; height: 14px;" />
|
||||
Integrations
|
||||
</button>
|
||||
<button :class="['ld-tab', activeTab === 'management' ? 'ld-tab--active' : '']" @click="activeTab = 'management'">
|
||||
<UIcon name="i-heroicons-cog-6-tooth" style="width: 14px; height: 14px;" />
|
||||
Management
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ══════ TAB: LEADS ══════ -->
|
||||
<template v-if="activeTab === 'leads'">
|
||||
<!-- Source filter pills -->
|
||||
<div class="ld-filter-row">
|
||||
<button
|
||||
v-for="src in SOURCES" :key="src.value"
|
||||
class="ld-pill" :class="sourceFilter === src.value ? 'ld-pill--active' : ''"
|
||||
@click="sourceFilter = src.value as any"
|
||||
>
|
||||
<UIcon :name="src.icon" style="width: 11px; height: 11px;" />
|
||||
{{ src.label }}
|
||||
<span v-if="sourceCounts[src.value]" class="ld-pill-count">{{ sourceCounts[src.value] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search + filters bar -->
|
||||
<div class="ld-toolbar">
|
||||
<UInput v-model="search" icon="i-heroicons-magnifying-glass" placeholder="Search leads..." class="ld-search" />
|
||||
<USelect v-model="statusFilter" :items="STATUS_OPTIONS" value-key="value" label-key="label" class="ld-select" />
|
||||
<USelect v-model="assignmentFilter" :items="[{value:'all',label:'All leads'},{value:'assigned',label:'Assigned'},{value:'unassigned',label:'Unassigned'}]" value-key="value" label-key="label" class="ld-select" />
|
||||
</div>
|
||||
|
||||
<!-- Lead list -->
|
||||
<div class="ld-card">
|
||||
<div class="ld-table-header">
|
||||
<span class="flex-1">Lead</span>
|
||||
<span class="ld-col-source">Source</span>
|
||||
<span class="ld-col-status">Status</span>
|
||||
<span class="ld-col-value">Value</span>
|
||||
<span class="ld-col-agent">Assigned to</span>
|
||||
<span class="ld-col-time">When</span>
|
||||
<span class="w-[60px]"></span>
|
||||
</div>
|
||||
|
||||
<div v-if="filtered.length === 0" class="ld-empty">
|
||||
<UIcon name="i-heroicons-funnel" style="width: 20px; height: 20px; color: #c0c0bc;" />
|
||||
<p>No leads match your filters.</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(lead, i) in filtered" :key="lead.id" class="ld-row" :class="i < filtered.length - 1 ? 'ld-row-border' : ''">
|
||||
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<div class="ld-avatar" :class="lead.assignedTo ? '' : 'ld-avatar--unassigned'">
|
||||
{{ lead.name.split(' ').map((w: string) => w[0]).join('').slice(0, 2) }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)] truncate">{{ lead.name }}</p>
|
||||
<span class="text-[10px] text-[var(--text-muted)]">{{ lead.lob }}</span>
|
||||
<span v-if="!lead.assignedTo" class="ld-unassigned-badge">Unassigned</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<span v-if="lead.phone" class="text-[10px] text-[var(--text-muted)] tabular-nums">{{ lead.phone }}</span>
|
||||
<template v-if="lead.phone && lead.email"><span class="ld-sep" /></template>
|
||||
<span v-if="lead.email" class="text-[10px] text-[var(--text-muted)] truncate">{{ lead.email }}</span>
|
||||
</div>
|
||||
<p v-if="lead.note || lead.campaignName" class="text-[10px] text-[var(--text-muted)] truncate opacity-60 mt-0.5">
|
||||
{{ lead.campaignName ? '📣 ' + lead.campaignName : lead.note }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ld-col-source">
|
||||
<span class="ld-source-chip">
|
||||
<UIcon :name="sourceIcon(lead.source)" style="width: 10px; height: 10px;" />
|
||||
{{ sourceLabel(lead.source) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ld-col-status">
|
||||
<span :class="statusBadge(lead.status).cls">{{ statusBadge(lead.status).label }}</span>
|
||||
</div>
|
||||
<div class="ld-col-value">
|
||||
<span v-if="lead.value" class="text-[11px] font-medium text-[var(--text-primary)] tabular-nums">{{ fmtValue(lead.value) }}</span>
|
||||
</div>
|
||||
<div class="ld-col-agent">
|
||||
<template v-if="lead.assignedTo">
|
||||
<span class="text-[11px] text-[var(--text-primary)]">{{ lead.assignedTo }}</span>
|
||||
<button class="ld-action-mini" title="Unassign" @click="unassignLead(lead)">
|
||||
<UIcon name="i-heroicons-x-mark" style="width: 10px; height: 10px;" />
|
||||
</button>
|
||||
</template>
|
||||
<select
|
||||
v-else
|
||||
class="ld-assign-native"
|
||||
@change="(e: Event) => { const v = (e.target as HTMLSelectElement).value; if (v) { assignLead(lead, v); (e.target as HTMLSelectElement).value = '' } }"
|
||||
>
|
||||
<option value="">Assign...</option>
|
||||
<option v-for="a in AGENTS" :key="a" :value="a">{{ a }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ld-col-time">
|
||||
<span class="text-[10px] text-[var(--text-muted)]">{{ timeAgo(lead.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="w-[60px] flex items-center justify-end gap-1">
|
||||
<NuxtLink to="/quotes/new" class="ld-action-btn" title="Start quote">
|
||||
<UIcon name="i-heroicons-calculator" style="width: 11px; height: 11px;" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ══════ TAB: INTEGRATIONS ══════ -->
|
||||
<template v-if="activeTab === 'integrations'">
|
||||
<p class="ld-section-hint">Connect lead sources to automatically import leads. APIs sync on a schedule when connected.</p>
|
||||
|
||||
<div class="ld-intg-grid">
|
||||
<div v-for="intg in integrations" :key="intg.id" class="ld-intg-card" :class="intg.connected ? 'ld-intg-card--connected' : ''">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="ld-intg-icon" :class="intg.connected ? 'ld-intg-icon--on' : ''">
|
||||
<UIcon :name="intg.icon" style="width: 18px; height: 18px;" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[13px] font-semibold text-[var(--text-primary)]">{{ intg.name }}</p>
|
||||
<p class="text-[11px] text-[var(--text-muted)] mt-0.5">{{ intg.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span v-if="intg.connected" class="ld-intg-stat">
|
||||
<UIcon name="i-heroicons-arrow-path" style="width: 10px; height: 10px;" />
|
||||
{{ intg.leadsCount }} leads
|
||||
</span>
|
||||
<span v-if="intg.connected && intg.lastSync" class="text-[10px] text-[var(--text-muted)]">
|
||||
Last sync: {{ timeAgo(intg.lastSync) }}
|
||||
</span>
|
||||
<span v-if="!intg.connected" class="text-[10px] text-[var(--text-muted)]">Not connected</span>
|
||||
</div>
|
||||
<button
|
||||
class="ld-intg-toggle" :class="intg.connected ? 'ld-intg-toggle--on' : ''"
|
||||
@click="toggleIntegration(intg)"
|
||||
>
|
||||
{{ intg.connected ? 'Disconnect' : 'Connect' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ══════ TAB: MANAGEMENT ══════ -->
|
||||
<template v-if="activeTab === 'management'">
|
||||
<div class="ld-mgmt-grid">
|
||||
<!-- Auto-assignment rules -->
|
||||
<div class="ld-card">
|
||||
<div class="ld-mgmt-header">
|
||||
<UIcon name="i-heroicons-arrow-path-rounded-square" style="width: 16px; height: 16px; color: #01696f;" />
|
||||
<h3 class="text-[14px] font-semibold text-[var(--text-primary)]">Auto-assignment rules</h3>
|
||||
</div>
|
||||
<p class="text-[11px] text-[var(--text-muted)] mt-1 mb-3 px-4">Automatically assign leads to agents by source and line of business.</p>
|
||||
|
||||
<div v-for="(rule, i) in autoAssignRules" :key="rule.id" class="ld-rule-row" :class="i < autoAssignRules.length - 1 ? 'ld-row-border' : ''">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span class="ld-source-chip">
|
||||
<UIcon :name="sourceIcon(rule.source as LeadSource)" style="width: 10px; height: 10px;" />
|
||||
{{ rule.source === 'all' ? 'All sources' : sourceLabel(rule.source as LeadSource) }}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-arrow-right" style="width: 10px; height: 10px; color: #8a8a86;" />
|
||||
<span class="text-[11px] text-[var(--text-muted)]">{{ rule.lob === 'all' ? 'Any LOB' : rule.lob }}</span>
|
||||
<UIcon name="i-heroicons-arrow-right" style="width: 10px; height: 10px; color: #8a8a86;" />
|
||||
<span class="text-[12px] font-medium text-[var(--text-primary)]">{{ rule.agent }}</span>
|
||||
</div>
|
||||
<USwitch :model-value="rule.enabled" @update:model-value="rule.enabled = $event" />
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3">
|
||||
<button class="ld-add-rule">
|
||||
<UIcon name="i-heroicons-plus" style="width: 12px; height: 12px;" />
|
||||
Add rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lead intake & release workflow -->
|
||||
<div class="ld-card">
|
||||
<div class="ld-mgmt-header">
|
||||
<UIcon name="i-heroicons-inbox-arrow-down" style="width: 16px; height: 16px; color: #01696f;" />
|
||||
<h3 class="text-[14px] font-semibold text-[var(--text-primary)]">Intake & release mode</h3>
|
||||
</div>
|
||||
<p class="text-[11px] text-[var(--text-muted)] mt-1 mb-3 px-4">Choose how incoming leads are handled when they arrive from any source.</p>
|
||||
|
||||
<div class="flex flex-col gap-2 px-4 pb-4">
|
||||
<label class="ld-mode-option" :class="releaseMode === 'auto_assign' ? 'ld-mode-option--active' : ''">
|
||||
<input type="radio" v-model="releaseMode" value="auto_assign" class="ld-radio" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Auto-assign by rules</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Leads matching rules above are assigned automatically. Unmatched leads go to the unassigned pool.</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="ld-mode-option" :class="releaseMode === 'reception' ? 'ld-mode-option--active' : ''">
|
||||
<input type="radio" v-model="releaseMode" value="reception" class="ld-radio" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Manual reception (monitored)</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">All leads held for a manager or receptionist to review and assign individually.</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="ld-mode-option" :class="releaseMode === 'auto_release' ? 'ld-mode-option--active' : ''">
|
||||
<input type="radio" v-model="releaseMode" value="auto_release" class="ld-radio" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Auto-release unassigned</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Pre-assigned leads (from rules) go to their agent. Everything else is released unassigned to the team pool for agents to claim.</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="ld-mode-option" :class="releaseMode === 'round_robin' ? 'ld-mode-option--active' : ''">
|
||||
<input type="radio" v-model="releaseMode" value="round_robin" class="ld-radio" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Round-robin to team</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Pre-assigned leads go to their agent. Remaining leads are distributed evenly across active agents in rotation.</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Release permissions & settings -->
|
||||
<div class="ld-card">
|
||||
<div class="ld-mgmt-header">
|
||||
<UIcon name="i-heroicons-shield-check" style="width: 16px; height: 16px; color: #01696f;" />
|
||||
<h3 class="text-[14px] font-semibold text-[var(--text-primary)]">Release permissions & notifications</h3>
|
||||
</div>
|
||||
<p class="text-[11px] text-[var(--text-muted)] mt-1 mb-3 px-4">Control who can release leads and how agents are notified.</p>
|
||||
|
||||
<div class="flex flex-col gap-3 px-4 pb-4">
|
||||
<label class="ld-setting-row">
|
||||
<div>
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Only managers can release held leads</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">When reception mode is on, only users with manager role can assign or release leads.</p>
|
||||
</div>
|
||||
<USwitch :model-value="true" />
|
||||
</label>
|
||||
<label class="ld-setting-row">
|
||||
<div>
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Allow agents to self-claim from pool</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Agents can pick up unassigned leads themselves. Turn off to require manager assignment only.</p>
|
||||
</div>
|
||||
<USwitch :model-value="true" />
|
||||
</label>
|
||||
<label class="ld-setting-row">
|
||||
<div>
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Auto-release stale leads</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Uncontacted leads older than 48h are returned to the unassigned pool.</p>
|
||||
</div>
|
||||
<USwitch :model-value="false" />
|
||||
</label>
|
||||
<label class="ld-setting-row">
|
||||
<div>
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Notify agents on assignment</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Push notification when a lead is assigned, reassigned, or released to pool.</p>
|
||||
</div>
|
||||
<USwitch :model-value="true" />
|
||||
</label>
|
||||
<label class="ld-setting-row">
|
||||
<div>
|
||||
<p class="text-[12px] font-medium text-[var(--text-primary)]">Notify manager on new unassigned leads</p>
|
||||
<p class="text-[10px] text-[var(--text-muted)]">Alert when leads arrive and no rule matches — useful in reception mode.</p>
|
||||
</div>
|
||||
<USwitch :model-value="true" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature toggle info -->
|
||||
<div class="ld-card ld-card-hint">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-information-circle" style="width: 14px; height: 14px; color: #01696f;" />
|
||||
<p class="text-[12px] text-[var(--text-muted)]">
|
||||
Leads Hub can be turned on or off in
|
||||
<NuxtLink to="/settings" class="text-[#01696f] font-medium hover:underline">Settings → Sidebar features</NuxtLink>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── ld- prefix: leads hub scoped styles ── */
|
||||
|
||||
.ld-page {
|
||||
display: flex; flex-direction: column; gap: 20px; padding-bottom: 3rem;
|
||||
max-width: 76rem; margin: 0 auto;
|
||||
}
|
||||
|
||||
/* KPI strip */
|
||||
.ld-kpi-strip {
|
||||
display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px;
|
||||
background: rgba(0,0,0,0.06); border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow: hidden;
|
||||
}
|
||||
@media (min-width: 640px) { .ld-kpi-strip { grid-template-columns: repeat(3, 1fr); } }
|
||||
@media (min-width: 1024px) { .ld-kpi-strip { grid-template-columns: repeat(5, 1fr); } }
|
||||
.ld-kpi { display: flex; align-items: center; gap: 10px; padding: 14px 18px; background: #fff; }
|
||||
.ld-kpi-icon {
|
||||
width: 34px; height: 34px; border-radius: 9px;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.ld-kpi-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #8a8a86; }
|
||||
.ld-kpi-value { font-size: 22px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* Tabs */
|
||||
.ld-tabs {
|
||||
display: flex; gap: 2px; background: rgba(0,0,0,0.04); border-radius: 10px; padding: 3px;
|
||||
}
|
||||
.ld-tab {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px;
|
||||
border-radius: 8px; font-size: 12px; font-weight: 500; color: var(--text-muted);
|
||||
background: transparent; border: none; cursor: pointer; transition: all 120ms ease;
|
||||
}
|
||||
.ld-tab:hover { color: var(--text-primary); }
|
||||
.ld-tab--active {
|
||||
background: #fff; color: var(--text-primary); font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Filter pills */
|
||||
.ld-filter-row { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; }
|
||||
.ld-pill {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 10px; border-radius: 9999px;
|
||||
font-size: 11px; font-weight: 500; white-space: nowrap;
|
||||
border: 1px solid rgba(0,0,0,0.08); background: transparent;
|
||||
color: var(--text-muted); cursor: pointer; transition: all 120ms ease;
|
||||
}
|
||||
.ld-pill:hover { background: rgba(0,0,0,0.03); }
|
||||
.ld-pill--active {
|
||||
background: rgba(1,105,111,0.08); color: #01696f;
|
||||
border-color: rgba(1,105,111,0.2); font-weight: 600;
|
||||
}
|
||||
.ld-pill-count {
|
||||
font-size: 9px; font-weight: 700; min-width: 14px; text-align: center;
|
||||
padding: 0 3px; border-radius: 9999px; background: rgba(0,0,0,0.05); color: var(--text-muted);
|
||||
}
|
||||
.ld-pill--active .ld-pill-count { background: rgba(1,105,111,0.12); color: #01696f; }
|
||||
|
||||
/* Toolbar */
|
||||
.ld-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.ld-search { flex: 1 1 220px; min-width: 180px; }
|
||||
.ld-select { width: 100%; max-width: 160px; }
|
||||
@media (max-width: 639px) { .ld-select { max-width: 100%; } }
|
||||
|
||||
/* Card */
|
||||
.ld-card {
|
||||
background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow: hidden;
|
||||
}
|
||||
.ld-card-hint { padding: 14px 16px; }
|
||||
|
||||
/* Table */
|
||||
.ld-table-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 14px; font-size: 10px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--text-muted); border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
background: rgba(0,0,0,0.015);
|
||||
}
|
||||
.ld-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 14px; transition: background 100ms ease;
|
||||
}
|
||||
.ld-row:hover { background: rgba(1,105,111,0.02); }
|
||||
.ld-row-border { border-bottom: 1px solid rgba(0,0,0,0.04); }
|
||||
|
||||
.ld-col-source { width: 90px; display: none; }
|
||||
.ld-col-status { width: 70px; display: none; }
|
||||
.ld-col-value { width: 70px; text-align: right; display: none; }
|
||||
.ld-col-agent { width: 120px; display: none; align-items: center; gap: 4px; }
|
||||
.ld-col-time { width: 60px; text-align: right; display: none; }
|
||||
@media (min-width: 640px) { .ld-col-source, .ld-col-status { display: flex; align-items: center; justify-content: center; } }
|
||||
@media (min-width: 768px) { .ld-col-value, .ld-col-time { display: flex; align-items: center; justify-content: flex-end; } }
|
||||
@media (min-width: 1024px) { .ld-col-agent { display: flex; } }
|
||||
|
||||
.ld-empty {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 4px; padding: 24px 12px; text-align: center;
|
||||
font-size: 12px; color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.ld-avatar {
|
||||
width: 28px; height: 28px; border-radius: 7px;
|
||||
background: rgba(1,105,111,0.07); color: #01696f;
|
||||
font-size: 10px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.ld-avatar--unassigned { background: rgba(194,123,26,0.08); color: #c27b1a; }
|
||||
|
||||
.ld-sep { width: 3px; height: 3px; border-radius: 50%; background: rgba(0,0,0,0.15); flex-shrink: 0; }
|
||||
|
||||
.ld-source-chip {
|
||||
display: inline-flex; align-items: center; gap: 3px;
|
||||
font-size: 10px; font-weight: 500; color: #01696f;
|
||||
padding: 1px 6px; border-radius: 9999px; background: rgba(1,105,111,0.06);
|
||||
}
|
||||
|
||||
.ld-unassigned-badge {
|
||||
font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px;
|
||||
background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.ld-status-new { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(59,130,246,0.08); color: #3b82f6; white-space: nowrap; }
|
||||
.ld-status-contacted { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(1,105,111,0.08); color: #01696f; white-space: nowrap; }
|
||||
.ld-status-qualified { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap; }
|
||||
.ld-status-proposal { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(139,92,246,0.08); color: #8b5cf6; white-space: nowrap; }
|
||||
.ld-status-won { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(15,123,95,0.08); color: #0f7b5f; white-space: nowrap; }
|
||||
.ld-status-lost { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 9999px; background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap; }
|
||||
|
||||
.ld-action-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 24px; height: 24px; border-radius: 6px;
|
||||
background: rgba(1,105,111,0.06); color: #01696f; flex-shrink: 0;
|
||||
transition: all 150ms ease; border: none; cursor: pointer;
|
||||
}
|
||||
.ld-action-btn:hover { background: rgba(1,105,111,0.14); }
|
||||
|
||||
.ld-action-mini {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 18px; height: 18px; border-radius: 4px;
|
||||
background: transparent; color: var(--text-muted); border: none; cursor: pointer;
|
||||
transition: all 100ms ease;
|
||||
}
|
||||
.ld-action-mini:hover { background: rgba(193,56,56,0.08); color: #c13838; }
|
||||
|
||||
.ld-assign-native {
|
||||
width: 90px; font-size: 10px; padding: 2px 4px; border-radius: 5px;
|
||||
border: 1px solid rgba(0,0,0,0.1); background: #fff; color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ld-assign-native:hover { border-color: rgba(1,105,111,0.3); }
|
||||
|
||||
/* ── Integrations tab ── */
|
||||
.ld-section-hint { font-size: 13px; color: var(--text-muted); }
|
||||
|
||||
.ld-intg-grid {
|
||||
display: grid; grid-template-columns: 1fr; gap: 12px;
|
||||
}
|
||||
@media (min-width: 768px) { .ld-intg-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (min-width: 1280px) { .ld-intg-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
|
||||
.ld-intg-card {
|
||||
background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 12px;
|
||||
padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
.ld-intg-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||
.ld-intg-card--connected { border-color: rgba(1,105,111,0.15); }
|
||||
|
||||
.ld-intg-icon {
|
||||
width: 38px; height: 38px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
background: rgba(0,0,0,0.04); color: #8a8a86;
|
||||
}
|
||||
.ld-intg-icon--on { background: rgba(1,105,111,0.08); color: #01696f; }
|
||||
|
||||
.ld-intg-stat {
|
||||
display: inline-flex; align-items: center; gap: 3px;
|
||||
font-size: 10px; font-weight: 600; color: #01696f;
|
||||
}
|
||||
|
||||
.ld-intg-toggle {
|
||||
font-size: 11px; font-weight: 600; padding: 4px 12px; border-radius: 6px;
|
||||
border: 1px solid rgba(0,0,0,0.1); background: transparent; color: var(--text-muted);
|
||||
cursor: pointer; transition: all 120ms ease;
|
||||
}
|
||||
.ld-intg-toggle:hover { border-color: rgba(0,0,0,0.2); }
|
||||
.ld-intg-toggle--on {
|
||||
background: rgba(1,105,111,0.06); color: #01696f; border-color: rgba(1,105,111,0.15);
|
||||
}
|
||||
|
||||
/* ── Management tab ── */
|
||||
.ld-mgmt-grid { display: flex; flex-direction: column; gap: 16px; }
|
||||
.ld-mgmt-header { display: flex; align-items: center; gap: 8px; padding: 14px 16px 0 16px; }
|
||||
|
||||
.ld-rule-row {
|
||||
display: flex; align-items: center; gap: 8px; padding: 10px 16px;
|
||||
}
|
||||
|
||||
.ld-setting-row {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 10px 14px; background: rgba(0,0,0,0.015); border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
.ld-setting-row:hover { background: rgba(0,0,0,0.025); }
|
||||
|
||||
.ld-mode-option {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 10px 14px; border-radius: 8px; cursor: pointer;
|
||||
border: 1px solid rgba(0,0,0,0.06); background: rgba(0,0,0,0.01);
|
||||
transition: all 120ms ease;
|
||||
}
|
||||
.ld-mode-option:hover { border-color: rgba(0,0,0,0.12); }
|
||||
.ld-mode-option--active {
|
||||
border-color: rgba(1,105,111,0.25); background: rgba(1,105,111,0.03);
|
||||
}
|
||||
.ld-radio {
|
||||
margin-top: 2px; accent-color: #01696f; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ld-add-rule {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
font-size: 12px; font-weight: 500; color: #01696f;
|
||||
background: none; border: none; cursor: pointer; padding: 0;
|
||||
}
|
||||
.ld-add-rule:hover { text-decoration: underline; }
|
||||
</style>
|
||||
519
app/pages/sales/quick-lead.vue
Normal file
519
app/pages/sales/quick-lead.vue
Normal file
@@ -0,0 +1,519 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuickLeads, type QuickLead } from '~/composables/useQuickLeads'
|
||||
|
||||
usePageTitle('Quick Leads')
|
||||
|
||||
const { leads, addLead, removeLead, recentLeads } = useQuickLeads()
|
||||
const toast = useToast()
|
||||
|
||||
/* ── Form state ── */
|
||||
const formOpen = ref(false)
|
||||
const name = ref('')
|
||||
const phone = ref('')
|
||||
const email = ref('')
|
||||
const product = ref('')
|
||||
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 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: '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 },
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
name.value = ''
|
||||
phone.value = ''
|
||||
email.value = ''
|
||||
product.value = ''
|
||||
source.value = ''
|
||||
priority.value = 'normal'
|
||||
note.value = ''
|
||||
formOpen.value = false
|
||||
}
|
||||
|
||||
function confirmRemove(id: string) {
|
||||
removeLead(id)
|
||||
toast.add({ title: 'Lead removed', color: 'neutral' })
|
||||
}
|
||||
|
||||
/* ── List filtering ── */
|
||||
type ListFilter = 'all' | 'urgent' | 'high' | 'normal'
|
||||
const activeFilter = ref<ListFilter>('all')
|
||||
|
||||
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,
|
||||
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,
|
||||
}))
|
||||
|
||||
/* ── 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' }
|
||||
}
|
||||
|
||||
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' })
|
||||
}
|
||||
|
||||
/* ── Seed demo data if empty ── */
|
||||
onMounted(() => {
|
||||
if (leads.value.length === 0) {
|
||||
const now = Date.now()
|
||||
const demoLeads: Omit<QuickLead, 'id'>[] = [
|
||||
{ name: 'Diego Herrera', phone: '+506 6100-4422', email: 'diego.h@gmail.com', product: 'Auto', source: 'referral', priority: 'high', note: 'Referred by Roberto Jiménez — needs fleet quote for 3 vehicles', agent: 'Marco V.', createdAt: new Date(now - 2 * 86400000).toISOString() },
|
||||
{ name: 'Valeria Núñez', phone: '+506 8899-1100', email: '', product: 'Auto', source: 'walk-in', priority: 'normal', note: 'Walk-in, interested in comprehensive auto', agent: 'Ana R.', createdAt: new Date(now - 4 * 86400000).toISOString() },
|
||||
{ name: 'Patricia Mora', phone: '+506 7700-3311', email: 'patricia.mora@empresa.cr', product: 'Health', source: 'website', priority: 'urgent', note: 'Corporate group health RFP — 20 employees, need response by Friday', agent: 'Ana R.', createdAt: new Date(now - 1 * 86400000).toISOString() },
|
||||
{ name: 'José Ramírez', phone: '+506 6045-8820', email: 'jramirez@outlook.com', product: 'Life', source: 'phone', priority: 'normal', note: 'Interested in term life, age 45', agent: 'Marco V.', createdAt: new Date(now - 6 * 86400000).toISOString() },
|
||||
{ name: 'Lucía Castillo', phone: '+506 8312-5500', email: '', product: 'Auto', source: 'social', priority: 'high', note: 'Instagram DM — new car, wants quote ASAP', agent: 'Marco V.', createdAt: new Date(now - 3 * 86400000).toISOString() },
|
||||
]
|
||||
// Insert with proper IDs
|
||||
for (const entry of demoLeads.reverse()) {
|
||||
leads.value = [{
|
||||
id: crypto.randomUUID?.() ?? String(Date.now() + Math.random()),
|
||||
...entry,
|
||||
}, ...leads.value]
|
||||
}
|
||||
}
|
||||
})
|
||||
</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">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">
|
||||
<label class="ql-label">Priority</label>
|
||||
<USelect v-model="priority" :items="priorityOptions" placeholder="Normal" 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" />
|
||||
</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() || !product ? '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(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>
|
||||
</div>
|
||||
<div class="ql-kpi">
|
||||
<p class="ql-kpi-label">High priority</p>
|
||||
<p class="ql-kpi-value" style="color: #c27b1a;">{{ filterCounts.high }}</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: 'urgent', label: 'Urgent' },
|
||||
{ id: 'high', label: 'High' },
|
||||
{ id: 'normal', label: 'Normal' },
|
||||
] 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="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>
|
||||
<span class="ql-product-tag">{{ lead.product }}</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ql-lead-actions">
|
||||
<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">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-ellipsis" style="width: 11px; height: 11px; color: #8a8a86; flex-shrink: 0;" />
|
||||
<span>{{ lead.note }}</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>
|
||||
</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;
|
||||
}
|
||||
.ql-product-tag {
|
||||
font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px;
|
||||
background: rgba(1,105,111,0.07); color: #01696f; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Empty ── */
|
||||
.ql-empty {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 40px 16px; text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user