995 lines
33 KiB
Vue
995 lines
33 KiB
Vue
<script setup lang="ts">
|
|
import { MOCK_CUSTOMERS, fmtMoney, customerTier, type MockCustomer, type CustomerTier } from '~/data/mock-customers'
|
|
|
|
usePageTitle('New Quote')
|
|
|
|
/* ── Pipeline bar ── */
|
|
const { deals: allDeals, getActiveDeal, createDeal } = useSalesPipeline()
|
|
const route = useRoute()
|
|
const activeDealId = ref<string | null>(route.query.deal as string | null)
|
|
|
|
const pipelineDeal = computed(() => {
|
|
if (activeDealId.value) return allDeals.value.find(d => d.id === activeDealId.value) ?? null
|
|
return null
|
|
})
|
|
|
|
// Active deals for the deal picker (exclude completed emissions)
|
|
const activeDeals = computed(() => allDeals.value.filter(d => d.currentStage !== 'emission').slice(0, 10))
|
|
|
|
function onPipelineNavigate(stage: string) {
|
|
// Navigation map for pipeline stages
|
|
const stageRoutes: Record<string, string> = {
|
|
customer: '/quotes/new',
|
|
get_quotes: '/quotes/new',
|
|
present_quotes: '/quotes/compare',
|
|
solicitud: '/onboarding/solicitud',
|
|
emission: '/onboarding/emissions',
|
|
}
|
|
const route = stageRoutes[stage]
|
|
if (route) navigateTo(route)
|
|
}
|
|
|
|
/* ── Steps ── */
|
|
type QuoteStep = 'customer' | 'product'
|
|
const currentStep = ref<QuoteStep>('customer')
|
|
|
|
/* ── Customer selection ── */
|
|
const customerSearch = ref('')
|
|
const selectedCustomer = ref<MockCustomer | null>(null)
|
|
const manualEntry = ref(false)
|
|
|
|
/* ── Tier filter ── */
|
|
type TierFilter = 'all' | CustomerTier
|
|
const activeTierFilter = ref<TierFilter>('all')
|
|
|
|
const tierMeta: Record<CustomerTier, { label: string; icon: string; color: string; bg: string; desc: string }> = {
|
|
quick_lead: { label: 'Quick Lead', icon: 'i-heroicons-bolt', color: '#c27b1a', bg: 'rgba(194,123,26,0.07)', desc: 'Name + contact only' },
|
|
lead: { label: 'Lead', icon: 'i-heroicons-user-plus', color: '#7c3aed', bg: 'rgba(124,58,237,0.07)', desc: 'Profile info, no policies' },
|
|
customer: { label: 'Customer', icon: 'i-heroicons-shield-check', color: '#01696f', bg: 'rgba(1,105,111,0.07)', desc: 'Active policies' },
|
|
cancelled: { label: 'Cancelled', icon: 'i-heroicons-x-circle', color: '#c13838', bg: 'rgba(193,56,56,0.07)', desc: 'All policies cancelled/lapsed' },
|
|
}
|
|
|
|
const tierFilterTabs = computed(() => {
|
|
const all = MOCK_CUSTOMERS
|
|
const counts: Record<TierFilter, number> = {
|
|
all: all.length,
|
|
quick_lead: all.filter(c => customerTier(c) === 'quick_lead').length,
|
|
lead: all.filter(c => customerTier(c) === 'lead').length,
|
|
customer: all.filter(c => customerTier(c) === 'customer').length,
|
|
cancelled: all.filter(c => customerTier(c) === 'cancelled').length,
|
|
}
|
|
return [
|
|
{ id: 'all' as TierFilter, label: 'All', count: counts.all },
|
|
{ id: 'customer' as TierFilter, label: 'Customers', count: counts.customer },
|
|
{ id: 'lead' as TierFilter, label: 'Leads', count: counts.lead },
|
|
{ id: 'quick_lead' as TierFilter, label: 'Quick Leads', count: counts.quick_lead },
|
|
{ id: 'cancelled' as TierFilter, label: 'Cancelled', count: counts.cancelled },
|
|
]
|
|
})
|
|
|
|
const filteredCustomers = computed(() => {
|
|
let list = MOCK_CUSTOMERS
|
|
|
|
// Tier filter
|
|
if (activeTierFilter.value !== 'all') {
|
|
list = list.filter(c => customerTier(c) === activeTierFilter.value)
|
|
}
|
|
|
|
// Search
|
|
const q = customerSearch.value.trim().toLowerCase()
|
|
if (q) {
|
|
list = list.filter(c =>
|
|
c.name.toLowerCase().includes(q) ||
|
|
c.email.toLowerCase().includes(q) ||
|
|
c.documentId.toLowerCase().includes(q) ||
|
|
c.id.toLowerCase().includes(q) ||
|
|
c.phone.includes(q) ||
|
|
c.tags.some(t => t.toLowerCase().includes(q))
|
|
)
|
|
}
|
|
|
|
return list
|
|
})
|
|
|
|
function selectCustomer(c: MockCustomer) {
|
|
selectedCustomer.value = c
|
|
currentStep.value = 'product'
|
|
}
|
|
|
|
function skipToManual() {
|
|
manualEntry.value = true
|
|
selectedCustomer.value = null
|
|
currentStep.value = 'product'
|
|
}
|
|
|
|
function goBackToCustomer() {
|
|
currentStep.value = 'customer'
|
|
}
|
|
|
|
/* ── Product line cards ── */
|
|
const lobCards: { id: string; label: string; hint: string; icon: string; to: string }[] = [
|
|
{ id: 'auto', label: 'Auto', hint: 'Vehicles, fleet, and liability coverage.', icon: 'i-heroicons-truck', to: '/quotes/auto' },
|
|
{ id: 'health', label: 'Health', hint: 'Individual, corporate, and group medical.', icon: 'i-heroicons-heart', to: '/quotes/health' },
|
|
{ id: 'life', label: 'Life', hint: 'Term, whole life, key person, and group.', icon: 'i-heroicons-shield-check', to: '/quotes/life' },
|
|
{ id: 'general_risk', label: 'General Risk', hint: 'Property, liability, marine, and specialty.', icon: 'i-heroicons-building-office-2', to: '/quotes/general-risk' },
|
|
{ id: 'custom', label: 'Custom', hint: 'Manual or non-standard product lines.', icon: 'i-heroicons-puzzle-piece', to: '/quotes/custom' },
|
|
]
|
|
|
|
/* ── Helpers ── */
|
|
function customerPolicySummary(c: MockCustomer) {
|
|
if (c.policies.length === 0) return 'No policies'
|
|
const lines = [...new Set(c.policies.map(p => p.line))]
|
|
return `${c.policies.length} ${c.policies.length === 1 ? 'policy' : 'policies'} · ${lines.join(', ')}`
|
|
}
|
|
|
|
function customerPaymentClass(c: MockCustomer) {
|
|
if (c.paymentStatus === 'Overdue') return 'nq-badge-warn'
|
|
if (c.paymentStatus === 'Grace period') return 'nq-badge-grace'
|
|
if (c.paymentStatus === 'N/A') return 'nq-badge-na'
|
|
return 'nq-badge-ok'
|
|
}
|
|
|
|
function tierOf(c: MockCustomer) {
|
|
return customerTier(c)
|
|
}
|
|
|
|
/** Compact info line depending on tier */
|
|
function customerInfoLine(c: MockCustomer): string {
|
|
const t = customerTier(c)
|
|
if (t === 'quick_lead') {
|
|
const parts: string[] = []
|
|
if (c.phone) parts.push(c.phone)
|
|
if (c.email) parts.push(c.email)
|
|
return parts.join(' · ') || 'Minimal info captured'
|
|
}
|
|
if (t === 'lead') {
|
|
return c.email + (c.address ? ` · ${c.address}` : '')
|
|
}
|
|
return c.email
|
|
}
|
|
|
|
/** Premium total (0 for leads) */
|
|
function customerPremium(c: MockCustomer): number {
|
|
return c.policies.reduce((s, p) => s + p.premium, 0)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="nq-page">
|
|
<!-- Back -->
|
|
<NuxtLink to="/quotes" class="inline-flex">
|
|
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Back to overview</UButton>
|
|
</NuxtLink>
|
|
|
|
<!-- Sales flow indicator -->
|
|
<SalesFlowIndicator current-stage="get_quotes" />
|
|
|
|
<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)]">Get Quotes</h1>
|
|
<p class="mt-1 max-w-2xl text-[13px] text-[var(--text-muted)]">
|
|
{{ currentStep === 'customer' ? 'Select a customer to quote, or enter details manually.' : 'Choose a product line to begin the quoting wizard.' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pipeline bar — active deal context -->
|
|
<div v-if="activeDeals.length > 0" class="nq-pipeline-zone">
|
|
<div v-if="!pipelineDeal" class="nq-deal-picker">
|
|
<div class="flex items-center gap-2 text-[12px] text-[var(--text-muted)]">
|
|
<UIcon name="i-heroicons-arrow-path" style="width: 13px; height: 13px; opacity: 0.5;" />
|
|
<span class="font-medium">Continue an active deal:</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2 mt-2">
|
|
<button
|
|
v-for="d in activeDeals" :key="d.id"
|
|
type="button"
|
|
class="nq-deal-chip"
|
|
@click="activeDealId = d.id"
|
|
>
|
|
<span class="font-semibold">{{ d.customerName.split(' ').slice(0, 2).join(' ') }}</span>
|
|
<span class="nq-deal-chip-line">{{ d.productLine }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<template v-else>
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-[11px] font-semibold uppercase tracking-wider text-[#8a8a86]">Active Deal</span>
|
|
<button type="button" class="text-[11px] text-[var(--text-muted)] hover:text-[var(--text-primary)]" @click="activeDealId = null">Switch deal</button>
|
|
</div>
|
|
<SalesPipelineBar :deal="pipelineDeal" @navigate="onPipelineNavigate" />
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Step indicator -->
|
|
<div class="nq-steps">
|
|
<div class="nq-step" :class="currentStep === 'customer' ? 'nq-step-active' : 'nq-step-done'" @click="goBackToCustomer">
|
|
<div class="nq-step-circle">
|
|
<UIcon v-if="currentStep === 'product'" name="i-heroicons-check" style="width: 12px; height: 12px;" />
|
|
<span v-else>1</span>
|
|
</div>
|
|
<span class="nq-step-label">Customer</span>
|
|
</div>
|
|
<div class="nq-step-line" :class="currentStep === 'product' ? 'nq-step-line-done' : ''" />
|
|
<div class="nq-step" :class="currentStep === 'product' ? 'nq-step-active' : ''">
|
|
<div class="nq-step-circle">2</div>
|
|
<span class="nq-step-label">Product</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ STEP 1: Customer selection ═══ -->
|
|
<template v-if="currentStep === 'customer'">
|
|
<!-- Manual entry option -->
|
|
<div class="nq-manual-banner">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="nq-manual-icon">
|
|
<UIcon name="i-heroicons-pencil-square" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-[13px] font-semibold text-[var(--text-primary)]">Manual quote entry</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">Skip customer selection and enter details directly in the quoting form.</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="nq-manual-btn" @click="skipToManual">
|
|
Enter manually
|
|
<UIcon name="i-heroicons-arrow-right" style="width: 12px; height: 12px;" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tier filter tabs + search -->
|
|
<div class="nq-filter-bar">
|
|
<div class="nq-tier-tabs">
|
|
<button
|
|
v-for="tab in tierFilterTabs"
|
|
:key="tab.id"
|
|
type="button"
|
|
class="nq-tier-tab"
|
|
:class="activeTierFilter === tab.id ? 'nq-tier-tab-on' : 'nq-tier-tab-off'"
|
|
@click="activeTierFilter = tab.id"
|
|
>
|
|
{{ tab.label }}
|
|
<span class="nq-tier-count" :class="activeTierFilter === tab.id ? 'nq-tier-count-on' : ''">{{ tab.count }}</span>
|
|
</button>
|
|
</div>
|
|
<div class="nq-search-wrap">
|
|
<UIcon name="i-heroicons-magnifying-glass" style="width: 15px; height: 15px; color: #8a8a86;" />
|
|
<input
|
|
v-model="customerSearch"
|
|
type="text"
|
|
class="nq-search"
|
|
placeholder="Search by name, email, phone, ID, or tag..."
|
|
/>
|
|
<kbd v-if="!customerSearch" class="nq-kbd">/</kbd>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results count -->
|
|
<div class="flex items-center justify-between gap-3">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86]">
|
|
{{ activeTierFilter === 'all' ? 'All contacts' : tierMeta[activeTierFilter as CustomerTier]?.label }}
|
|
</p>
|
|
<span class="text-[11px] text-[var(--text-muted)]">{{ filteredCustomers.length }} results</span>
|
|
</div>
|
|
|
|
<!-- Customer cards -->
|
|
<div class="nq-customer-grid">
|
|
<button
|
|
v-for="c in filteredCustomers"
|
|
:key="c.id"
|
|
type="button"
|
|
class="nq-customer-card"
|
|
:class="tierOf(c) === 'cancelled' ? 'nq-card-cancelled' : ''"
|
|
@click="selectCustomer(c)"
|
|
>
|
|
<!-- Top row: avatar + name + tier badge -->
|
|
<div class="nq-customer-top">
|
|
<div class="nq-customer-avatar" :style="{ background: tierMeta[tierOf(c)].bg, color: tierMeta[tierOf(c)].color }">
|
|
{{ c.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">{{ c.name }}</p>
|
|
<!-- Tier badge -->
|
|
<span
|
|
class="nq-tier-badge"
|
|
:style="{ background: tierMeta[tierOf(c)].bg, color: tierMeta[tierOf(c)].color }"
|
|
>
|
|
<UIcon :name="tierMeta[tierOf(c)].icon" style="width: 10px; height: 10px;" />
|
|
{{ tierMeta[tierOf(c)].label }}
|
|
</span>
|
|
<!-- Payment status (only for customers) -->
|
|
<span v-if="tierOf(c) === 'customer'" :class="customerPaymentClass(c)">{{ c.paymentStatus }}</span>
|
|
</div>
|
|
<p class="text-[11px] text-[var(--text-muted)] truncate mt-0.5">{{ customerInfoLine(c) }}</p>
|
|
</div>
|
|
<UIcon name="i-heroicons-chevron-right" style="width: 16px; height: 16px; color: #c0c0bc; flex-shrink: 0;" />
|
|
</div>
|
|
|
|
<!-- Meta row — adapts to tier -->
|
|
<div class="nq-customer-meta">
|
|
<!-- Quick lead: just tags -->
|
|
<template v-if="tierOf(c) === 'quick_lead'">
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Source</span>
|
|
<span class="nq-stat-value">{{ c.tags.filter(t => t !== 'Quick lead').join(', ') || '—' }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Added</span>
|
|
<span class="nq-stat-value">{{ c.since }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Agent</span>
|
|
<span class="nq-stat-value">{{ c.agent }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Lead: profile data -->
|
|
<template v-else-if="tierOf(c) === 'lead'">
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">ID</span>
|
|
<span class="nq-stat-value">{{ c.documentId }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Type</span>
|
|
<span class="nq-stat-value">{{ c.type }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Agent</span>
|
|
<span class="nq-stat-value">{{ c.agent }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Added</span>
|
|
<span class="nq-stat-value">{{ c.since }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Customer / Cancelled: full stats -->
|
|
<template v-else>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">ID</span>
|
|
<span class="nq-stat-value">{{ c.documentId }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Type</span>
|
|
<span class="nq-stat-value">{{ c.type }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Premium</span>
|
|
<span class="nq-stat-value">{{ customerPremium(c) > 0 ? fmtMoney(customerPremium(c)) + '/yr' : '—' }}</span>
|
|
</div>
|
|
<div class="nq-customer-stat">
|
|
<span class="nq-stat-label">Agent</span>
|
|
<span class="nq-stat-value">{{ c.agent }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Policies line (customers + cancelled) -->
|
|
<div v-if="c.policies.length > 0" class="nq-customer-policies">
|
|
<UIcon name="i-heroicons-document-text" style="width: 12px; height: 12px; color: #8a8a86; flex-shrink: 0;" />
|
|
<span class="text-[11px] text-[var(--text-muted)]">{{ customerPolicySummary(c) }}</span>
|
|
<!-- Cancelled inline warning -->
|
|
<span v-if="tierOf(c) === 'cancelled'" class="nq-cancelled-hint">
|
|
<UIcon name="i-heroicons-exclamation-triangle" style="width: 10px; height: 10px;" />
|
|
All policies inactive
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Activity hint for leads -->
|
|
<div v-if="(tierOf(c) === 'lead' || tierOf(c) === 'quick_lead') && c.activity.length > 0" class="nq-customer-policies">
|
|
<UIcon name="i-heroicons-clock" style="width: 12px; height: 12px; color: #8a8a86; flex-shrink: 0;" />
|
|
<span class="text-[11px] text-[var(--text-muted)]">{{ c.activity[0]?.text }}</span>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div v-if="c.tags.length > 0" class="nq-tag-row">
|
|
<span
|
|
v-for="tag in c.tags.slice(0, 3)"
|
|
:key="tag"
|
|
class="nq-tag"
|
|
>{{ tag }}</span>
|
|
<span v-if="c.tags.length > 3" class="nq-tag nq-tag-more">+{{ c.tags.length - 3 }}</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="filteredCustomers.length === 0" class="nq-empty">
|
|
<UIcon name="i-heroicons-user-group" style="width: 32px; height: 32px; color: #c0c0bc;" />
|
|
<p class="text-[13px] text-[var(--text-muted)] mt-2">No contacts match your search.</p>
|
|
<button type="button" class="nq-manual-btn mt-3" @click="skipToManual">Enter details manually</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ═══ STEP 2: Product line selection ═══ -->
|
|
<template v-if="currentStep === 'product'">
|
|
<!-- Selected customer summary -->
|
|
<div v-if="selectedCustomer" class="nq-selected-banner" :class="tierOf(selectedCustomer) === 'cancelled' ? 'nq-selected-cancelled' : ''">
|
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
|
<div class="nq-customer-avatar" :style="{ background: tierMeta[tierOf(selectedCustomer)].bg, color: tierMeta[tierOf(selectedCustomer)].color }">
|
|
{{ selectedCustomer.name.split(' ').map(w => w[0]).join('').slice(0, 2) }}
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">{{ selectedCustomer.name }}</p>
|
|
<span
|
|
class="nq-tier-badge"
|
|
:style="{ background: tierMeta[tierOf(selectedCustomer)].bg, color: tierMeta[tierOf(selectedCustomer)].color }"
|
|
>
|
|
<UIcon :name="tierMeta[tierOf(selectedCustomer)].icon" style="width: 10px; height: 10px;" />
|
|
{{ tierMeta[tierOf(selectedCustomer)].label }}
|
|
</span>
|
|
</div>
|
|
<p class="text-[12px] text-[var(--text-muted)]">
|
|
{{ customerInfoLine(selectedCustomer) }}
|
|
<template v-if="tierOf(selectedCustomer) === 'customer'"> · {{ customerPolicySummary(selectedCustomer) }}</template>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="nq-change-btn" @click="goBackToCustomer">
|
|
<UIcon name="i-heroicons-arrows-right-left" style="width: 12px; height: 12px;" />
|
|
Change
|
|
</button>
|
|
</div>
|
|
<div v-else class="nq-selected-banner nq-selected-manual">
|
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
|
<div class="nq-manual-avatar">
|
|
<UIcon name="i-heroicons-pencil-square" style="width: 16px; height: 16px;" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">Manual entry</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">Customer details will be entered in the quoting form.</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="nq-change-btn" @click="goBackToCustomer">
|
|
<UIcon name="i-heroicons-arrows-right-left" style="width: 12px; height: 12px;" />
|
|
Select customer instead
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Cancelled customer warning -->
|
|
<div v-if="selectedCustomer && tierOf(selectedCustomer) === 'cancelled'" class="nq-cancelled-banner">
|
|
<UIcon name="i-heroicons-exclamation-triangle" style="width: 16px; height: 16px; color: #c13838; flex-shrink: 0;" />
|
|
<div class="min-w-0">
|
|
<p class="text-[13px] font-semibold text-[#c13838]">Cancelled customer</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">All previous policies are cancelled or lapsed. This is a win-back opportunity — proceed with a fresh quote.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick lead / lead info hint -->
|
|
<div v-if="selectedCustomer && (tierOf(selectedCustomer) === 'quick_lead' || tierOf(selectedCustomer) === 'lead')" class="nq-lead-hint-banner">
|
|
<UIcon name="i-heroicons-information-circle" style="width: 16px; height: 16px; color: #7c3aed; flex-shrink: 0;" />
|
|
<div class="min-w-0">
|
|
<p class="text-[13px] font-semibold" style="color: #5b21b6;">
|
|
{{ tierOf(selectedCustomer) === 'quick_lead' ? 'Quick lead — limited info' : 'Lead — no existing policies' }}
|
|
</p>
|
|
<p class="text-[12px] text-[var(--text-muted)]">
|
|
{{ tierOf(selectedCustomer) === 'quick_lead'
|
|
? 'Only basic contact info was captured. The quoting form will need all details filled manually.'
|
|
: 'This contact has a full profile but no policies yet. Profile data will pre-fill the quoting form.'
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Existing policies hint (customers only) -->
|
|
<div v-if="selectedCustomer && tierOf(selectedCustomer) === 'customer' && selectedCustomer.policies.length > 0" class="nq-existing">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86] mb-2">Existing coverage</p>
|
|
<div class="nq-existing-pills">
|
|
<span v-for="p in selectedCustomer.policies" :key="p.id" class="nq-existing-pill">
|
|
<span class="nq-existing-dot" />
|
|
{{ p.line }} · {{ p.carrier }} · {{ fmtMoney(p.premium) }}
|
|
</span>
|
|
</div>
|
|
<p class="text-[11px] text-[var(--text-muted)] mt-2">Consider cross-sell opportunities in lines not currently covered.</p>
|
|
</div>
|
|
|
|
<!-- Product line cards -->
|
|
<div class="nq-section">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86] mb-3">Select product line</p>
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<NuxtLink
|
|
v-for="card in lobCards"
|
|
:key="card.id"
|
|
:to="card.to + (selectedCustomer ? `?customer=${selectedCustomer.id}` : '')"
|
|
class="nq-lob-card group"
|
|
>
|
|
<div class="nq-lob-icon">
|
|
<UIcon :name="card.icon" class="h-5 w-5" />
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="font-semibold text-[var(--text-primary)] group-hover:text-[#01696f]">{{ card.label }}</p>
|
|
<p class="mt-1 text-sm text-[var(--text-muted)]">{{ card.hint }}</p>
|
|
</div>
|
|
<UIcon
|
|
name="i-heroicons-chevron-right"
|
|
class="ml-auto mt-0.5 h-4 w-4 shrink-0 text-[var(--text-muted)] opacity-0 transition group-hover:opacity-100"
|
|
/>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<UAlert
|
|
color="neutral"
|
|
variant="soft"
|
|
icon="i-heroicons-information-circle"
|
|
title="What is a quote?"
|
|
description="A quote collects the risk details, selects carriers to solicit, and tracks the response — single or comparative. Customer data will be pre-filled from the selected profile."
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Pipeline zone ── */
|
|
.nq-pipeline-zone {
|
|
padding: 0;
|
|
}
|
|
.nq-deal-picker {
|
|
padding: 12px 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.nq-deal-chip {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 6px 12px; border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
|
font-size: 12px; color: var(--text-primary);
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.nq-deal-chip:hover { border-color: rgba(1,105,111,0.2); box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
|
|
.nq-deal-chip-line {
|
|
font-size: 10px; font-weight: 600; padding: 0 5px;
|
|
border-radius: 9999px; background: rgba(1,105,111,0.07); color: #01696f;
|
|
}
|
|
|
|
.nq-page {
|
|
max-width: 64rem;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
padding-bottom: 3rem;
|
|
}
|
|
|
|
/* ── Steps ── */
|
|
.nq-steps {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
padding: 0 4px;
|
|
}
|
|
.nq-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
.nq-step:hover { opacity: 0.8; }
|
|
.nq-step-circle {
|
|
width: 28px; height: 28px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
border: 2px solid rgba(0,0,0,0.1);
|
|
color: #8a8a86;
|
|
background: #fff;
|
|
transition: all 200ms ease;
|
|
}
|
|
.nq-step-active .nq-step-circle {
|
|
background: #01696f;
|
|
border-color: #01696f;
|
|
color: #fff;
|
|
}
|
|
.nq-step-done .nq-step-circle {
|
|
background: rgba(1,105,111,0.08);
|
|
border-color: #01696f;
|
|
color: #01696f;
|
|
}
|
|
.nq-step-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #8a8a86;
|
|
}
|
|
.nq-step-active .nq-step-label { color: var(--text-primary); font-weight: 600; }
|
|
.nq-step-done .nq-step-label { color: #01696f; }
|
|
|
|
.nq-step-line {
|
|
flex: 1;
|
|
height: 2px;
|
|
margin: 0 12px;
|
|
background: rgba(0,0,0,0.08);
|
|
border-radius: 1px;
|
|
transition: background 300ms ease;
|
|
}
|
|
.nq-step-line-done { background: #01696f; }
|
|
|
|
/* ── Manual entry banner ── */
|
|
.nq-manual-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 14px 18px;
|
|
border-radius: 10px;
|
|
border: 1px dashed rgba(0,0,0,0.1);
|
|
background: rgba(0,0,0,0.015);
|
|
flex-wrap: wrap;
|
|
}
|
|
.nq-manual-icon {
|
|
width: 36px; height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
color: #8a8a86;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.nq-manual-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 12px;
|
|
border-radius: 7px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.nq-manual-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
|
|
/* ── Filter bar ── */
|
|
.nq-filter-bar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.nq-tier-tabs {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
overflow-x: auto;
|
|
}
|
|
.nq-tier-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;
|
|
}
|
|
.nq-tier-tab-on {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.nq-tier-tab-off {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
.nq-tier-tab-off:hover { color: var(--text-primary); }
|
|
.nq-tier-count {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 5px;
|
|
border-radius: 9999px;
|
|
background: rgba(0,0,0,0.06);
|
|
color: var(--text-muted);
|
|
}
|
|
.nq-tier-count-on {
|
|
background: rgba(1,105,111,0.1);
|
|
color: #01696f;
|
|
}
|
|
|
|
/* ── Search ── */
|
|
.nq-section {}
|
|
.nq-search-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
transition: border-color 150ms ease;
|
|
}
|
|
.nq-search-wrap:focus-within { border-color: #01696f; }
|
|
.nq-search {
|
|
flex: 1;
|
|
border: none;
|
|
outline: none;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
background: transparent;
|
|
}
|
|
.nq-search::placeholder { color: #8a8a86; }
|
|
.nq-kbd {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
background: rgba(0,0,0,0.04);
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
color: #8a8a86;
|
|
font-family: inherit;
|
|
}
|
|
|
|
/* ── Customer cards ── */
|
|
.nq-customer-grid {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
.nq-customer-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
padding: 14px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.nq-customer-card:hover {
|
|
border-color: rgba(1,105,111,0.2);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
}
|
|
.nq-card-cancelled {
|
|
border-color: rgba(193,56,56,0.08);
|
|
background: rgba(193,56,56,0.01);
|
|
}
|
|
.nq-card-cancelled:hover {
|
|
border-color: rgba(193,56,56,0.2);
|
|
}
|
|
.nq-customer-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.nq-customer-avatar {
|
|
width: 36px; height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(1,105,111,0.08);
|
|
color: #01696f;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.nq-customer-meta {
|
|
display: flex;
|
|
gap: 16px;
|
|
padding-left: 46px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.nq-customer-stat { display: flex; flex-direction: column; gap: 1px; }
|
|
.nq-stat-label { font-size: 10px; font-weight: 500; color: #8a8a86; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
.nq-stat-value { font-size: 12px; font-weight: 500; color: var(--text-primary); font-variant-numeric: tabular-nums; }
|
|
|
|
.nq-customer-policies {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding-left: 46px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* ── Tier badge ── */
|
|
.nq-tier-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 1px 7px;
|
|
border-radius: 9999px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
/* ── Tags ── */
|
|
.nq-tag-row {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding-left: 46px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.nq-tag {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
padding: 1px 7px;
|
|
border-radius: 4px;
|
|
background: rgba(0,0,0,0.04);
|
|
color: #8a8a86;
|
|
}
|
|
.nq-tag-more {
|
|
background: rgba(0,0,0,0.06);
|
|
color: #6b6b68;
|
|
}
|
|
|
|
/* ── Cancelled hint ── */
|
|
.nq-cancelled-hint {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: #c13838;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
/* ── Badges ── */
|
|
.nq-badge-ok {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(15,123,95,0.06); color: #0f7b5f; white-space: nowrap;
|
|
}
|
|
.nq-badge-warn {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(193,56,56,0.06); color: #c13838; white-space: nowrap;
|
|
}
|
|
.nq-badge-grace {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(194,123,26,0.06); color: #c27b1a; white-space: nowrap;
|
|
}
|
|
.nq-badge-na {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap;
|
|
}
|
|
|
|
/* ── Empty ── */
|
|
.nq-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Selected customer banner ── */
|
|
.nq-selected-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(1,105,111,0.15);
|
|
background: rgba(1,105,111,0.02);
|
|
flex-wrap: wrap;
|
|
}
|
|
.nq-selected-cancelled {
|
|
border-color: rgba(193,56,56,0.15);
|
|
background: rgba(193,56,56,0.02);
|
|
}
|
|
.nq-selected-manual {
|
|
border-color: rgba(0,0,0,0.08);
|
|
background: rgba(0,0,0,0.015);
|
|
border-style: dashed;
|
|
}
|
|
.nq-change-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 5px 10px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.nq-change-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.nq-manual-avatar {
|
|
width: 36px; height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
color: #8a8a86;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Contextual banners ── */
|
|
.nq-cancelled-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
background: rgba(193,56,56,0.03);
|
|
border: 1px solid rgba(193,56,56,0.1);
|
|
}
|
|
.nq-lead-hint-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
background: rgba(124,58,237,0.03);
|
|
border: 1px solid rgba(124,58,237,0.1);
|
|
}
|
|
|
|
/* ── Existing coverage ── */
|
|
.nq-existing {
|
|
padding: 14px 16px;
|
|
border-radius: 10px;
|
|
background: rgba(124,58,237,0.02);
|
|
border: 1px solid rgba(124,58,237,0.08);
|
|
}
|
|
.nq-existing-pills {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
.nq-existing-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px;
|
|
border-radius: 6px;
|
|
background: rgba(124,58,237,0.05);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #5b21b6;
|
|
}
|
|
.nq-existing-dot {
|
|
width: 5px; height: 5px;
|
|
border-radius: 50%;
|
|
background: #7c3aed;
|
|
}
|
|
|
|
/* ── LOB cards ── */
|
|
.nq-lob-card {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 12px;
|
|
padding: 18px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
transition: all 200ms ease;
|
|
text-decoration: none;
|
|
}
|
|
.nq-lob-card:hover {
|
|
border-color: rgba(1,105,111,0.2);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
|
}
|
|
.nq-lob-icon {
|
|
width: 44px; height: 44px;
|
|
border-radius: 12px;
|
|
background: rgba(1,105,111,0.06);
|
|
color: #01696f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: background 200ms ease;
|
|
}
|
|
.nq-lob-card:hover .nq-lob-icon { background: rgba(1,105,111,0.1); }
|
|
</style>
|