1268 lines
46 KiB
Vue
1268 lines
46 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
MOCK_PIPELINE_QUOTES,
|
|
QUOTE_LOB_OPTIONS,
|
|
UNIFIED_PIPELINE_STAGES,
|
|
type MockPipelineQuote,
|
|
type QuoteOverviewLob,
|
|
type QuotePipelineScopeFilter,
|
|
type UnifiedStageId
|
|
} from '~/data/quotes-overview.mock'
|
|
import { PIPELINE_STAGES, type PipelineStage } from '~/composables/useSalesPipeline'
|
|
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Quotes · Mission Control')
|
|
|
|
/* ── Top-level view: overview vs analytics ── */
|
|
type TopView = 'overview' | 'analytics'
|
|
const topView = ref<TopView>('overview')
|
|
|
|
/* ── Overview sub-view: tracker (3 buckets) vs pipeline (5 readable columns) ── */
|
|
type OverviewMode = 'tracker' | 'pipeline'
|
|
const overviewMode = ref<OverviewMode>('tracker')
|
|
|
|
/* ── Filters ── */
|
|
const pipelineScope = ref<QuotePipelineScopeFilter>('global')
|
|
const lobFilter = ref<QuoteOverviewLob | 'all'>('all')
|
|
const search = ref('')
|
|
|
|
const lobSelectItems = computed(() =>
|
|
QUOTE_LOB_OPTIONS.map((o) => ({ label: o.label, value: o.value }))
|
|
)
|
|
|
|
const scopeTabs = [
|
|
{ value: 'global' as const, label: 'Global', icon: 'i-heroicons-globe-alt' },
|
|
{ value: 'corporate' as const, label: 'Corporate', icon: 'i-heroicons-building-office-2' },
|
|
{ value: 'personal' as const, label: 'Personal', icon: 'i-heroicons-user' }
|
|
]
|
|
|
|
const filteredQuotes = computed(() => {
|
|
let rows = [...MOCK_PIPELINE_QUOTES]
|
|
if (pipelineScope.value !== 'global') {
|
|
rows = rows.filter((q) => q.party === pipelineScope.value)
|
|
}
|
|
if (lobFilter.value !== 'all') {
|
|
rows = rows.filter((q) => q.lob === lobFilter.value)
|
|
}
|
|
const t = search.value.trim().toLowerCase()
|
|
if (t) {
|
|
rows = rows.filter(
|
|
(q) =>
|
|
q.customerLabel.toLowerCase().includes(t) ||
|
|
q.id.toLowerCase().includes(t) ||
|
|
q.owner.toLowerCase().includes(t)
|
|
)
|
|
}
|
|
return rows
|
|
})
|
|
|
|
/* ── KPIs ── */
|
|
const kpis = computed(() => {
|
|
const q = filteredQuotes.value
|
|
const awaiting = q.filter((x) => !x.customerInformed).length
|
|
const formsOpen = q.filter((x) => x.formsDone < x.formsTotal).length
|
|
const avgFormPct = q.length > 0 ? Math.round(q.reduce((s, x) => s + formsPct(x), 0) / q.length) : 0
|
|
return { total: q.length, awaiting, formsOpen, avgFormPct }
|
|
})
|
|
|
|
/* ── Tracker: 3 buckets ── */
|
|
type TrackerBucket = 'waiting_quotes' | 'customer_review' | 'form_filling'
|
|
|
|
const TRACKER_BUCKETS: { id: TrackerBucket; label: string; hint: string; icon: string; color: string; stages: UnifiedStageId[] }[] = [
|
|
{ id: 'waiting_quotes', label: 'Waiting for quotes', hint: 'Gathering info & waiting on carriers', icon: 'i-heroicons-clock', color: '#f59e0b', stages: ['customer', 'get_quotes', 'waiting_carriers'] },
|
|
{ id: 'customer_review', label: 'Customer reviewing', hint: 'Quotes in — client deciding', icon: 'i-heroicons-eye', color: '#3b82f6', stages: ['present_quotes', 'waiting_client'] },
|
|
{ id: 'form_filling', label: 'Accepted — forms', hint: 'Customer said yes — paperwork time', icon: 'i-heroicons-document-check', color: '#10b981', stages: ['solicitud', 'emission'] },
|
|
]
|
|
|
|
const quotesByBucket = computed(() => {
|
|
const map: Record<TrackerBucket, MockPipelineQuote[]> = {
|
|
waiting_quotes: [], customer_review: [], form_filling: [],
|
|
}
|
|
for (const q of filteredQuotes.value) {
|
|
for (const bucket of TRACKER_BUCKETS) {
|
|
if (bucket.stages.includes(q.unifiedStage)) {
|
|
map[bucket.id].push(q)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return map
|
|
})
|
|
|
|
/* ── Pipeline: 5 readable columns (collapsed waiting stages) ── */
|
|
type PipelineCol = 'customer' | 'quoting' | 'presenting' | 'solicitud' | 'emission'
|
|
|
|
const PIPELINE_COLS: { id: PipelineCol; label: string; hint: string; stages: UnifiedStageId[]; color: string }[] = [
|
|
{ id: 'customer', label: 'Customer', hint: 'Intake & qualify', stages: ['customer'], color: '#8a8a86' },
|
|
{ id: 'quoting', label: 'Quoting', hint: 'Get quotes + awaiting carriers', stages: ['get_quotes', 'waiting_carriers'], color: '#01696f' },
|
|
{ id: 'presenting', label: 'Presenting', hint: 'Present + awaiting client', stages: ['present_quotes', 'waiting_client'], color: '#8b5cf6' },
|
|
{ id: 'solicitud', label: 'Solicitud', hint: 'Forms & binding', stages: ['solicitud'], color: '#d97706' },
|
|
{ id: 'emission', label: 'Emission', hint: 'Policy issued', stages: ['emission'], color: '#10b981' },
|
|
]
|
|
|
|
const quotesByCol = computed(() => {
|
|
const map: Record<PipelineCol, MockPipelineQuote[]> = {
|
|
customer: [], quoting: [], presenting: [], solicitud: [], emission: [],
|
|
}
|
|
for (const q of filteredQuotes.value) {
|
|
for (const col of PIPELINE_COLS) {
|
|
if (col.stages.includes(q.unifiedStage)) {
|
|
map[col.id].push(q)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return map
|
|
})
|
|
|
|
/* ── Analytics: mock data from mission control ── */
|
|
interface AgentStats {
|
|
name: string
|
|
activeDeals: number
|
|
quotesSent: number
|
|
conversionRate: number
|
|
avgResponseTime: string
|
|
pipelineValue: number
|
|
}
|
|
|
|
const agents: AgentStats[] = [
|
|
{ name: 'Ana Ramírez', activeDeals: 8, quotesSent: 14, conversionRate: 71, avgResponseTime: '1.2d', pipelineValue: 18_400 },
|
|
{ name: 'Marco Villanueva', activeDeals: 4, quotesSent: 6, conversionRate: 83, avgResponseTime: '0.8d', pipelineValue: 22_100 },
|
|
{ name: 'Lucía Fernández', activeDeals: 6, quotesSent: 4, conversionRate: 42, avgResponseTime: '2.4d', pipelineValue: 4_700 },
|
|
]
|
|
|
|
interface MockDeal {
|
|
id: string; customerName: string; productLine: string; currentStage: PipelineStage
|
|
agent: string; daysAtStage: number; premium: number; formCompletion: number
|
|
}
|
|
|
|
const mockDeals: MockDeal[] = [
|
|
{ id: 'd01', customerName: 'María Elena Pérez', productLine: 'Auto', currentStage: 'customer', agent: 'Ana Ramírez', daysAtStage: 1, premium: 1200, formCompletion: 45 },
|
|
{ id: 'd02', customerName: 'Carlos Mendoza', productLine: 'Life', currentStage: 'customer', agent: 'Lucía Fernández', daysAtStage: 3, premium: 3200, formCompletion: 20 },
|
|
{ id: 'd03', customerName: 'Laura Castillo', productLine: 'Health', currentStage: 'get_quotes', agent: 'Ana Ramírez', daysAtStage: 2, premium: 2800, formCompletion: 60 },
|
|
{ id: 'd04', customerName: 'Sofía Rojas Delgado', productLine: 'Auto', currentStage: 'get_quotes', agent: 'Lucía Fernández', daysAtStage: 4, premium: 1500, formCompletion: 50 },
|
|
{ id: 'd05', customerName: 'Andrés Vargas', productLine: 'General Risk', currentStage: 'get_quotes', agent: 'Marco Villanueva', daysAtStage: 1, premium: 8500, formCompletion: 75 },
|
|
{ id: 'd06', customerName: 'Patricia Herrera', productLine: 'Auto', currentStage: 'waiting_carriers', agent: 'Ana Ramírez', daysAtStage: 6, premium: 1100, formCompletion: 100 },
|
|
{ id: 'd07', customerName: 'Fernando López', productLine: 'Life', currentStage: 'waiting_carriers', agent: 'Lucía Fernández', daysAtStage: 8, premium: 4200, formCompletion: 100 },
|
|
{ id: 'd08', customerName: 'Roberto Jiménez Mora', productLine: 'Health', currentStage: 'waiting_carriers', agent: 'Marco Villanueva', daysAtStage: 3, premium: 3100, formCompletion: 100 },
|
|
{ id: 'd09', customerName: 'Gabriela Torres', productLine: 'Auto', currentStage: 'present_quotes', agent: 'Ana Ramírez', daysAtStage: 2, premium: 1400, formCompletion: 100 },
|
|
{ id: 'd10', customerName: 'Diego Salazar', productLine: 'Life', currentStage: 'present_quotes', agent: 'Marco Villanueva', daysAtStage: 1, premium: 5600, formCompletion: 100 },
|
|
{ id: 'd11', customerName: 'Isabel Moreno', productLine: 'General Risk', currentStage: 'waiting_client', agent: 'Ana Ramírez', daysAtStage: 7, premium: 2200, formCompletion: 100 },
|
|
{ id: 'd12', customerName: 'Alejandro Rios', productLine: 'Auto', currentStage: 'waiting_client', agent: 'Lucía Fernández', daysAtStage: 12, premium: 900, formCompletion: 100 },
|
|
{ id: 'd13', customerName: 'Valentina Cruz', productLine: 'Health', currentStage: 'solicitud', agent: 'Ana Ramírez', daysAtStage: 3, premium: 2700, formCompletion: 68 },
|
|
{ id: 'd14', customerName: 'Jorge Navarro', productLine: 'Life', currentStage: 'solicitud', agent: 'Marco Villanueva', daysAtStage: 2, premium: 6800, formCompletion: 85 },
|
|
{ id: 'd15', customerName: 'Camila Fuentes', productLine: 'Auto', currentStage: 'emission', agent: 'Ana Ramírez', daysAtStage: 1, premium: 1600, formCompletion: 92 },
|
|
{ id: 'd16', customerName: 'Raúl Espinoza', productLine: 'General Risk', currentStage: 'emission', agent: 'Marco Villanueva', daysAtStage: 2, premium: 7200, formCompletion: 100 },
|
|
{ id: 'd17', customerName: 'Mónica Delgado', productLine: 'Life', currentStage: 'waiting_carriers', agent: 'Ana Ramírez', daysAtStage: 11, premium: 3800, formCompletion: 100 },
|
|
{ id: 'd18', customerName: 'Enrique Paredes', productLine: 'Health', currentStage: 'customer', agent: 'Lucía Fernández', daysAtStage: 6, premium: 2100, formCompletion: 30 },
|
|
]
|
|
|
|
const totalPipelinePremium = computed(() =>
|
|
mockDeals.reduce((sum, d) => sum + d.premium, 0),
|
|
)
|
|
|
|
const stageCounts = computed(() => {
|
|
const counts: Record<PipelineStage, number> = {} as any
|
|
for (const s of PIPELINE_STAGES) counts[s.id] = 0
|
|
for (const d of mockDeals) counts[d.currentStage]++
|
|
return counts
|
|
})
|
|
|
|
const maxStageCount = computed(() =>
|
|
Math.max(...PIPELINE_STAGES.map(s => stageCounts.value[s.id]), 1),
|
|
)
|
|
|
|
const stageColors: Record<PipelineStage, string> = {
|
|
customer: '#01696f',
|
|
get_quotes: '#0d8a8f',
|
|
waiting_carriers: '#f59e0b',
|
|
present_quotes: '#3b82f6',
|
|
waiting_client: '#f59e0b',
|
|
solicitud: '#8b5cf6',
|
|
emission: '#10b981',
|
|
}
|
|
|
|
const stuckDeals = computed(() =>
|
|
mockDeals.filter(d => d.daysAtStage > 5).sort((a, b) => b.daysAtStage - a.daysAtStage),
|
|
)
|
|
|
|
const activeStageTab = ref<PipelineStage>('customer')
|
|
const stageDeals = computed(() =>
|
|
mockDeals.filter(d => d.currentStage === activeStageTab.value),
|
|
)
|
|
|
|
/* ── Helpers ── */
|
|
function lobBadgeColor(lob: QuoteOverviewLob) {
|
|
switch (lob) { case 'auto': return 'primary'; case 'health': return 'success'; case 'life': return 'info'; case 'general_risk': return 'warning'; default: return 'neutral' }
|
|
}
|
|
function lobLabel(lob: QuoteOverviewLob) { return QUOTE_LOB_OPTIONS.find((o) => o.value === lob)?.label ?? lob }
|
|
function lobIcon(lob: QuoteOverviewLob) {
|
|
const icons: Record<QuoteOverviewLob, string> = { auto: 'i-heroicons-truck', health: 'i-heroicons-heart', life: 'i-heroicons-shield-check', general_risk: 'i-heroicons-building-office-2', custom: 'i-heroicons-puzzle-piece' }
|
|
return icons[lob]
|
|
}
|
|
function lobHubPath(lob: QuoteOverviewLob) {
|
|
const paths: Record<QuoteOverviewLob, string> = { auto: '/quotes/auto', health: '/quotes/health', life: '/quotes/life', general_risk: '/quotes/general-risk', custom: '/quotes/custom' }
|
|
return paths[lob]
|
|
}
|
|
function formsPct(q: MockPipelineQuote) { return q.formsTotal <= 0 ? 0 : Math.round((100 * q.formsDone) / q.formsTotal) }
|
|
function fmtCurrency(v: number) { return '$' + v.toLocaleString('en-US') }
|
|
function stageLabelFor(id: PipelineStage) { return PIPELINE_STAGES.find(s => s.id === id)?.label ?? id }
|
|
function severityColor(days: number) { return days > 10 ? '#ef4444' : days > 5 ? '#f59e0b' : '#6b7280' }
|
|
const toast = useToast()
|
|
const nudgeConfirmDeal = ref<MockDeal | null>(null)
|
|
|
|
function onNudge(deal: MockDeal) {
|
|
nudgeConfirmDeal.value = deal
|
|
}
|
|
|
|
function confirmNudge() {
|
|
if (nudgeConfirmDeal.value) {
|
|
toast.add({ title: `Nudge sent to ${nudgeConfirmDeal.value.customerName}`, color: 'success' })
|
|
}
|
|
nudgeConfirmDeal.value = null
|
|
}
|
|
|
|
function cancelNudge() {
|
|
nudgeConfirmDeal.value = null
|
|
}
|
|
|
|
/* ── SVG ring for KPI ── */
|
|
const ringCirc = 2 * Math.PI * 38
|
|
const avgFormRing = computed(() => {
|
|
const pct = kpis.value.avgFormPct
|
|
return { dasharray: `${ringCirc}`, dashoffset: `${ringCirc - (ringCirc * pct) / 100}` }
|
|
})
|
|
|
|
const showStickyQuoteCta = ref(false)
|
|
onMounted(() => {
|
|
const onScroll = () => { showStickyQuoteCta.value = window.scrollY > 160 }
|
|
window.addEventListener('scroll', onScroll, { passive: true })
|
|
onUnmounted(() => window.removeEventListener('scroll', onScroll))
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="qmc-page">
|
|
<!-- Header -->
|
|
<div class="qmc-header">
|
|
<div>
|
|
<h1 class="qmc-title">Mission Control</h1>
|
|
<p class="qmc-subtitle">Pipeline overview, quoting tracker, and team analytics — all in one place.</p>
|
|
</div>
|
|
<div class="qmc-header-actions">
|
|
<NuxtLink to="/quotes/new">
|
|
<UButton icon="i-heroicons-plus" color="primary">New quote</UButton>
|
|
</NuxtLink>
|
|
<NuxtLink to="/quotes/compare">
|
|
<UButton icon="i-heroicons-squares-plus" color="neutral" variant="soft">Comparative</UButton>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TOP-LEVEL TOGGLE ═══ -->
|
|
<div class="qmc-view-toggle">
|
|
<button
|
|
type="button"
|
|
class="qmc-vtab"
|
|
:class="{ 'qmc-vtab--active': topView === 'overview' }"
|
|
@click="topView = 'overview'"
|
|
>
|
|
<UIcon name="i-heroicons-view-columns" class="qmc-vtab-icon" />
|
|
Quotes Overview
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="qmc-vtab"
|
|
:class="{ 'qmc-vtab--active': topView === 'analytics' }"
|
|
@click="topView = 'analytics'"
|
|
>
|
|
<UIcon name="i-heroicons-chart-bar" class="qmc-vtab-icon" />
|
|
Analytics
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════
|
|
OVERVIEW TAB
|
|
═══════════════════════════════════════════════════ -->
|
|
<template v-if="topView === 'overview'">
|
|
<!-- KPI Cards (modern) -->
|
|
<div class="qmc-kpi-grid">
|
|
<!-- Ring card -->
|
|
<div class="qmc-kpi-hero">
|
|
<div class="qmc-ring-wrap">
|
|
<svg viewBox="0 0 88 88" class="qmc-ring-svg">
|
|
<circle cx="44" cy="44" r="38" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="5" />
|
|
<circle
|
|
cx="44" cy="44" r="38" fill="none"
|
|
stroke="#5eead4" stroke-width="5" stroke-linecap="round"
|
|
:stroke-dasharray="avgFormRing.dasharray"
|
|
:stroke-dashoffset="avgFormRing.dashoffset"
|
|
transform="rotate(-90 44 44)"
|
|
class="qmc-ring-progress"
|
|
/>
|
|
</svg>
|
|
<span class="qmc-ring-label">{{ kpis.avgFormPct }}%</span>
|
|
</div>
|
|
<div class="qmc-kpi-hero-text">
|
|
<span class="qmc-kpi-title">Avg form completion</span>
|
|
<span class="qmc-kpi-sub">Across {{ kpis.total }} active quotes</span>
|
|
</div>
|
|
</div>
|
|
<!-- Stat cards -->
|
|
<div class="qmc-kpi-card">
|
|
<UIcon name="i-heroicons-document-text" class="qmc-kpi-icon" />
|
|
<span class="qmc-kpi-num">{{ kpis.total }}</span>
|
|
<span class="qmc-kpi-title">In-flight quotes</span>
|
|
</div>
|
|
<div class="qmc-kpi-card qmc-kpi-card--amber">
|
|
<UIcon name="i-heroicons-bell-alert" class="qmc-kpi-icon" />
|
|
<span class="qmc-kpi-num">{{ kpis.awaiting }}</span>
|
|
<span class="qmc-kpi-title">Need notification</span>
|
|
</div>
|
|
<div class="qmc-kpi-card">
|
|
<UIcon name="i-heroicons-clipboard-document-list" class="qmc-kpi-icon" />
|
|
<span class="qmc-kpi-num">{{ kpis.formsOpen }}</span>
|
|
<span class="qmc-kpi-title">Forms incomplete</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scope + filter row -->
|
|
<div class="qmc-filter-bar">
|
|
<div class="qmc-scope-pills">
|
|
<button
|
|
v-for="tab in scopeTabs"
|
|
:key="tab.value"
|
|
type="button"
|
|
class="qmc-scope-pill"
|
|
:class="{ 'qmc-scope-pill--active': pipelineScope === tab.value }"
|
|
@click="pipelineScope = tab.value"
|
|
>
|
|
<UIcon :name="tab.icon" style="width: 13px; height: 13px;" />
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
<div class="qmc-filter-inputs">
|
|
<USelect
|
|
v-model="lobFilter"
|
|
:items="lobSelectItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
class="w-full min-w-[10rem] sm:max-w-[14rem]"
|
|
/>
|
|
<UInput
|
|
v-model="search"
|
|
icon="i-heroicons-magnifying-glass"
|
|
placeholder="Search..."
|
|
class="w-full flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overview sub-toggle -->
|
|
<div class="qmc-sub-toggle-row">
|
|
<div class="qmc-sub-toggle">
|
|
<button
|
|
type="button"
|
|
class="qmc-stab"
|
|
:class="{ 'qmc-stab--active': overviewMode === 'tracker' }"
|
|
@click="overviewMode = 'tracker'"
|
|
>
|
|
Quote tracker
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="qmc-stab"
|
|
:class="{ 'qmc-stab--active': overviewMode === 'pipeline' }"
|
|
@click="overviewMode = 'pipeline'"
|
|
>
|
|
Full pipeline
|
|
</button>
|
|
</div>
|
|
<span class="qmc-sub-hint">
|
|
{{ overviewMode === 'tracker' ? '3 lifecycle stages' : '5 pipeline stages' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- TRACKER (3-bucket) -->
|
|
<div v-if="overviewMode === 'tracker'" class="qmc-tracker-grid">
|
|
<div
|
|
v-for="bucket in TRACKER_BUCKETS"
|
|
:key="bucket.id"
|
|
class="qmc-bucket"
|
|
>
|
|
<div class="qmc-bucket-head" :style="`--bc: ${bucket.color}`">
|
|
<div class="qmc-bucket-icon" :style="`background: ${bucket.color}14; color: ${bucket.color}`">
|
|
<UIcon :name="bucket.icon" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="qmc-bucket-info">
|
|
<div class="qmc-bucket-row">
|
|
<span class="qmc-bucket-label">{{ bucket.label }}</span>
|
|
<span class="qmc-bucket-count" :style="`background: ${bucket.color}14; color: ${bucket.color}`">
|
|
{{ quotesByBucket[bucket.id].length }}
|
|
</span>
|
|
</div>
|
|
<span class="qmc-bucket-hint">{{ bucket.hint }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="qmc-bucket-cards">
|
|
<div
|
|
v-for="q in quotesByBucket[bucket.id]"
|
|
:key="q.id"
|
|
class="qmc-qcard"
|
|
>
|
|
<div class="qmc-qcard-top">
|
|
<div class="qmc-qcard-name-wrap">
|
|
<p class="qmc-qcard-name">{{ q.customerLabel }}</p>
|
|
<p class="qmc-qcard-meta">{{ q.pathLabel }} · {{ q.owner }}</p>
|
|
</div>
|
|
<UBadge :color="lobBadgeColor(q.lob)" variant="soft" size="xs">{{ lobLabel(q.lob) }}</UBadge>
|
|
</div>
|
|
<div class="qmc-qcard-bar-row">
|
|
<span class="qmc-qcard-bar-label">{{ q.formsDone }}/{{ q.formsTotal }}</span>
|
|
<div class="qmc-qcard-bar-track">
|
|
<div class="qmc-qcard-bar-fill" :style="{ width: `${formsPct(q)}%`, background: bucket.color }" />
|
|
</div>
|
|
</div>
|
|
<div class="qmc-qcard-tags">
|
|
<UBadge :color="q.customerInformed ? 'success' : 'warning'" variant="soft" size="xs">
|
|
{{ q.customerInformed ? 'Informed' : 'Nudge' }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
<p v-if="quotesByBucket[bucket.id].length === 0" class="qmc-empty-col">No quotes here</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PIPELINE (5 columns) -->
|
|
<div v-if="overviewMode === 'pipeline'" class="qmc-pipeline-scroll">
|
|
<div class="qmc-pipeline-grid">
|
|
<div
|
|
v-for="col in PIPELINE_COLS"
|
|
:key="col.id"
|
|
class="qmc-pcol"
|
|
>
|
|
<div class="qmc-pcol-head">
|
|
<div class="qmc-pcol-dot" :style="`background: ${col.color}`" />
|
|
<span class="qmc-pcol-label">{{ col.label }}</span>
|
|
<span class="qmc-pcol-count">{{ quotesByCol[col.id].length }}</span>
|
|
</div>
|
|
<p class="qmc-pcol-hint">{{ col.hint }}</p>
|
|
<div class="qmc-pcol-cards">
|
|
<div
|
|
v-for="q in quotesByCol[col.id]"
|
|
:key="q.id"
|
|
class="qmc-qcard"
|
|
>
|
|
<div class="qmc-qcard-top">
|
|
<div class="qmc-qcard-name-wrap">
|
|
<p class="qmc-qcard-name">{{ q.customerLabel }}</p>
|
|
<p class="qmc-qcard-meta">{{ q.pathLabel }} · {{ q.owner }}</p>
|
|
</div>
|
|
<UBadge :color="lobBadgeColor(q.lob)" variant="soft" size="xs">{{ lobLabel(q.lob) }}</UBadge>
|
|
</div>
|
|
<div class="qmc-qcard-bar-row">
|
|
<span class="qmc-qcard-bar-label">{{ q.formsDone }}/{{ q.formsTotal }}</span>
|
|
<div class="qmc-qcard-bar-track">
|
|
<div class="qmc-qcard-bar-fill" :style="{ width: `${formsPct(q)}%`, background: col.color }" />
|
|
</div>
|
|
</div>
|
|
<div class="qmc-qcard-tags">
|
|
<UBadge :color="q.customerInformed ? 'success' : 'warning'" variant="soft" size="xs">
|
|
{{ q.customerInformed ? 'Informed' : 'Nudge' }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
<p v-if="quotesByCol[col.id].length === 0" class="qmc-empty-col">No quotes</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOB quick links -->
|
|
<div class="qmc-section">
|
|
<p class="qmc-section-eyebrow">Lines of business</p>
|
|
<div class="qmc-lob-grid">
|
|
<NuxtLink
|
|
v-for="lob in ['auto', 'health', 'life', 'general_risk', 'custom'] as const"
|
|
:key="lob"
|
|
:to="lobHubPath(lob)"
|
|
class="qmc-lob-card group"
|
|
>
|
|
<div class="qmc-lob-icon">
|
|
<UIcon :name="lobIcon(lob)" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<span class="qmc-lob-name">{{ lobLabel(lob) }}</span>
|
|
<UIcon name="i-heroicons-chevron-right" class="qmc-lob-arrow" />
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ═══════════════════════════════════════════════════
|
|
ANALYTICS TAB (from mission control)
|
|
═══════════════════════════════════════════════════ -->
|
|
<template v-if="topView === 'analytics'">
|
|
<!-- Analytics KPI strip (modern cards) -->
|
|
<div class="qmc-analytics-kpis">
|
|
<div class="qmc-akpi">
|
|
<span class="qmc-akpi-label">Active Deals</span>
|
|
<span class="qmc-akpi-value">{{ mockDeals.length }}</span>
|
|
</div>
|
|
<div class="qmc-akpi">
|
|
<span class="qmc-akpi-label">Quotes This Month</span>
|
|
<span class="qmc-akpi-value">24</span>
|
|
</div>
|
|
<div class="qmc-akpi">
|
|
<span class="qmc-akpi-label">Avg Days in Pipeline</span>
|
|
<span class="qmc-akpi-value">8.3</span>
|
|
</div>
|
|
<div class="qmc-akpi">
|
|
<span class="qmc-akpi-label">Conversion</span>
|
|
<span class="qmc-akpi-value qmc-akpi-value--brand">62%</span>
|
|
</div>
|
|
<div class="qmc-akpi">
|
|
<span class="qmc-akpi-label">Pipeline Premium</span>
|
|
<span class="qmc-akpi-value">{{ fmtCurrency(totalPipelinePremium) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Funnel chart -->
|
|
<div class="qmc-card qmc-section">
|
|
<h2 class="qmc-section-title">Pipeline funnel</h2>
|
|
<div class="qmc-funnel">
|
|
<div
|
|
v-for="stage in PIPELINE_STAGES"
|
|
:key="stage.id"
|
|
class="qmc-funnel-row"
|
|
>
|
|
<span class="qmc-funnel-label" :class="{ 'qmc-funnel-label--waiting': stage.isWaiting }">
|
|
{{ stage.label }}
|
|
<span v-if="stage.isWaiting" class="qmc-funnel-wait-dot" />
|
|
</span>
|
|
<div class="qmc-funnel-bar-track">
|
|
<div
|
|
class="qmc-funnel-bar"
|
|
:style="{
|
|
width: Math.max((stageCounts[stage.id] / maxStageCount) * 100, 4) + '%',
|
|
background: `linear-gradient(90deg, ${stageColors[stage.id]}, ${stageColors[stage.id]}cc)`,
|
|
}"
|
|
/>
|
|
</div>
|
|
<span class="qmc-funnel-count">{{ stageCounts[stage.id] }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agent performance -->
|
|
<div class="qmc-card qmc-section">
|
|
<h2 class="qmc-section-title">Agent performance</h2>
|
|
<div class="qmc-agent-grid">
|
|
<div v-for="a in agents" :key="a.name" class="qmc-agent-card">
|
|
<div class="qmc-agent-head">
|
|
<div class="qmc-agent-avatar">{{ a.name.split(' ').map(w => w[0]).join('') }}</div>
|
|
<div>
|
|
<p class="qmc-agent-name">{{ a.name }}</p>
|
|
<p class="qmc-agent-sub">{{ a.activeDeals }} deals · {{ a.quotesSent }} quotes</p>
|
|
</div>
|
|
</div>
|
|
<div class="qmc-agent-stats">
|
|
<div class="qmc-agent-stat">
|
|
<span class="qmc-agent-stat-label">Conversion</span>
|
|
<span
|
|
class="qmc-agent-stat-badge"
|
|
:style="{ background: a.conversionRate >= 70 ? 'rgba(16,185,129,0.1)' : a.conversionRate >= 50 ? 'rgba(245,158,11,0.1)' : 'rgba(239,68,68,0.1)', color: a.conversionRate >= 70 ? '#059669' : a.conversionRate >= 50 ? '#d97706' : '#dc2626' }"
|
|
>{{ a.conversionRate }}%</span>
|
|
</div>
|
|
<div class="qmc-agent-stat">
|
|
<span class="qmc-agent-stat-label">Avg response</span>
|
|
<span class="qmc-agent-stat-val">{{ a.avgResponseTime }}</span>
|
|
</div>
|
|
<div class="qmc-agent-stat">
|
|
<span class="qmc-agent-stat-label">Pipeline value</span>
|
|
<span class="qmc-agent-stat-val">{{ fmtCurrency(a.pipelineValue) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottleneck analysis -->
|
|
<div class="qmc-card qmc-section">
|
|
<h2 class="qmc-section-title">Bottleneck analysis</h2>
|
|
<p class="qmc-section-sub">Deals stuck at a stage for more than 5 days</p>
|
|
<div v-if="stuckDeals.length === 0" class="qmc-empty-state">No bottlenecks detected.</div>
|
|
<div v-else class="qmc-stuck-list">
|
|
<div
|
|
v-for="deal in stuckDeals"
|
|
:key="deal.id"
|
|
class="qmc-stuck-row"
|
|
:style="{ borderLeftColor: severityColor(deal.daysAtStage) }"
|
|
>
|
|
<div class="qmc-stuck-info">
|
|
<span class="qmc-stuck-name">{{ deal.customerName }}</span>
|
|
<span class="qmc-stuck-meta">
|
|
<span class="qmc-stuck-days" :style="{ color: severityColor(deal.daysAtStage) }">{{ deal.daysAtStage }}d</span>
|
|
at {{ stageLabelFor(deal.currentStage) }}
|
|
</span>
|
|
</div>
|
|
<span class="qmc-stuck-agent">{{ deal.agent }}</span>
|
|
<button class="qmc-btn-nudge" @click="onNudge(deal)">Nudge</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stage breakdown -->
|
|
<div class="qmc-card qmc-section">
|
|
<h2 class="qmc-section-title">Stage breakdown</h2>
|
|
<div class="qmc-stage-tabs">
|
|
<button
|
|
v-for="stage in PIPELINE_STAGES"
|
|
:key="stage.id"
|
|
class="qmc-stab"
|
|
:class="{ 'qmc-stab--active': activeStageTab === stage.id }"
|
|
@click="activeStageTab = stage.id"
|
|
>
|
|
{{ stage.label }}
|
|
</button>
|
|
</div>
|
|
<div v-if="stageDeals.length === 0" class="qmc-empty-state">
|
|
<UIcon name="i-heroicons-inbox" style="width: 20px; height: 20px; color: #c0c0bc;" />
|
|
<p>No deals at this stage</p>
|
|
<p class="text-[10px] opacity-60">Deals will appear here as they move through the pipeline.</p>
|
|
</div>
|
|
<div v-else class="qmc-stage-list">
|
|
<div v-for="deal in stageDeals" :key="deal.id" class="qmc-stage-row">
|
|
<div class="qmc-stage-row-main">
|
|
<span class="qmc-stage-row-name">{{ deal.customerName }}</span>
|
|
<span class="qmc-stage-row-line">{{ deal.productLine }}</span>
|
|
</div>
|
|
<span class="qmc-stage-row-days" :style="{ color: deal.daysAtStage > 5 ? severityColor(deal.daysAtStage) : 'inherit' }">
|
|
{{ deal.daysAtStage }}d
|
|
</span>
|
|
<span class="qmc-stage-row-agent">{{ deal.agent }}</span>
|
|
<span class="qmc-stage-row-prem">{{ fmtCurrency(deal.premium) }}</span>
|
|
<div class="qmc-stage-row-bar">
|
|
<div class="qmc-stage-bar-track">
|
|
<div class="qmc-stage-bar-fill" :style="{ width: deal.formCompletion + '%' }" />
|
|
</div>
|
|
<span class="qmc-stage-bar-pct">{{ deal.formCompletion }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Nudge confirmation modal -->
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="transition duration-150 ease-out"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition duration-100 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div v-if="nudgeConfirmDeal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30" @click.self="cancelNudge">
|
|
<div class="w-full max-w-sm rounded-xl border border-[var(--card-border)] bg-white p-6 shadow-xl">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style="background: rgba(1,105,111,0.08); color: #01696f;">
|
|
<UIcon name="i-heroicons-bell-alert" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">Send nudge?</p>
|
|
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
|
This will send a follow-up reminder to <span class="font-semibold text-[var(--text-primary)]">{{ nudgeConfirmDeal.customerName }}</span> about their pending deal.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 flex justify-end gap-2">
|
|
<UButton color="neutral" variant="soft" size="sm" @click="cancelNudge">Cancel</UButton>
|
|
<UButton color="primary" size="sm" @click="confirmNudge">Send nudge</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
|
|
<!-- Sticky "New quote" CTA -->
|
|
<Transition
|
|
enter-active-class="transition duration-200 ease-out"
|
|
enter-from-class="opacity-0 translate-y-2"
|
|
enter-to-class="opacity-100 translate-y-0"
|
|
leave-active-class="transition duration-150 ease-in"
|
|
leave-from-class="opacity-100 translate-y-0"
|
|
leave-to-class="opacity-0 translate-y-2"
|
|
>
|
|
<div v-if="showStickyQuoteCta" class="fixed bottom-6 right-6 z-40">
|
|
<NuxtLink to="/quotes/new">
|
|
<UButton icon="i-heroicons-plus" color="primary" size="lg" class="shadow-lg">New quote</UButton>
|
|
</NuxtLink>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ══════════════════════════════════════════════════════
|
|
QUOTES MISSION CONTROL — unified page
|
|
══════════════════════════════════════════════════════ */
|
|
|
|
.qmc-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
padding-bottom: 3rem;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.qmc-header {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.qmc-title {
|
|
font-size: 24px;
|
|
font-weight: 650;
|
|
letter-spacing: -0.015em;
|
|
color: var(--text-primary);
|
|
margin-top: 4px;
|
|
}
|
|
.qmc-subtitle {
|
|
font-size: 14px;
|
|
color: #8a8a86;
|
|
margin-top: 4px;
|
|
max-width: 36rem;
|
|
line-height: 1.5;
|
|
}
|
|
.qmc-header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* ── Top-level view toggle ── */
|
|
.qmc-view-toggle {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
}
|
|
.qmc-vtab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 16px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #6b6b68;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.qmc-vtab:hover { color: var(--text-primary); }
|
|
.qmc-vtab--active {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.qmc-vtab-icon { width: 15px; height: 15px; }
|
|
|
|
/* ── Modern KPI grid ── */
|
|
.qmc-kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: 1.4fr repeat(3, 1fr);
|
|
gap: 14px;
|
|
}
|
|
.qmc-kpi-hero {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
background: linear-gradient(135deg, #0a2e30 0%, #01696f 100%);
|
|
color: #fff;
|
|
}
|
|
.qmc-ring-wrap {
|
|
position: relative;
|
|
width: 72px;
|
|
height: 72px;
|
|
flex-shrink: 0;
|
|
}
|
|
.qmc-ring-svg { width: 100%; height: 100%; }
|
|
.qmc-ring-progress { transition: stroke-dashoffset 0.6s ease; }
|
|
.qmc-ring-label {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
color: #5eead4;
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
|
}
|
|
.qmc-kpi-hero-text { display: flex; flex-direction: column; gap: 2px; }
|
|
.qmc-kpi-hero-text .qmc-kpi-title { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.95); }
|
|
.qmc-kpi-hero-text .qmc-kpi-sub { font-size: 11px; color: rgba(255,255,255,0.6); }
|
|
|
|
.qmc-kpi-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 16px 12px;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.qmc-kpi-icon { width: 20px; height: 20px; color: #01696f; opacity: 0.6; }
|
|
.qmc-kpi-card--amber .qmc-kpi-icon { color: #d97706; }
|
|
.qmc-kpi-num { font-size: 28px; font-weight: 700; color: var(--text-primary); line-height: 1; }
|
|
.qmc-kpi-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #8a8a86; text-align: center; }
|
|
.qmc-kpi-sub { font-size: 11px; color: #8a8a86; }
|
|
|
|
/* ── Filter bar ── */
|
|
.qmc-filter-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.qmc-scope-pills {
|
|
display: inline-flex;
|
|
gap: 4px;
|
|
}
|
|
.qmc-scope-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 5px 12px;
|
|
border-radius: 9999px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.qmc-scope-pill:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.qmc-scope-pill--active {
|
|
background: rgba(1,105,111,0.06);
|
|
border-color: rgba(1,105,111,0.2);
|
|
color: #01696f;
|
|
}
|
|
.qmc-filter-inputs {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ── Sub-toggle (tracker / pipeline) ── */
|
|
.qmc-sub-toggle-row { display: flex; align-items: center; gap: 10px; }
|
|
.qmc-sub-toggle {
|
|
display: inline-flex;
|
|
gap: 1px;
|
|
padding: 2px;
|
|
border-radius: 8px;
|
|
background: rgba(0,0,0,0.04);
|
|
}
|
|
.qmc-stab {
|
|
padding: 5px 12px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border: none;
|
|
background: transparent;
|
|
color: #6b6b68;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 150ms ease;
|
|
}
|
|
.qmc-stab--active {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
}
|
|
.qmc-sub-hint { font-size: 11px; color: #8a8a86; }
|
|
|
|
/* ── Tracker grid (3 buckets) ── */
|
|
.qmc-tracker-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
}
|
|
.qmc-bucket {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow: hidden;
|
|
}
|
|
.qmc-bucket-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
}
|
|
.qmc-bucket-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
.qmc-bucket-info { min-width: 0; flex: 1; }
|
|
.qmc-bucket-row { display: flex; align-items: center; gap: 8px; }
|
|
.qmc-bucket-label { font-size: 14px; font-weight: 600; color: var(--text-primary); }
|
|
.qmc-bucket-count {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 20px;
|
|
min-width: 20px;
|
|
padding: 0 6px;
|
|
border-radius: 9999px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.qmc-bucket-hint { font-size: 11px; color: #8a8a86; margin-top: 1px; }
|
|
.qmc-bucket-cards {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
flex: 1;
|
|
}
|
|
|
|
/* ── Quote card (shared between tracker & pipeline) ── */
|
|
.qmc-qcard {
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: var(--page-bg, #fafafa);
|
|
transition: all 150ms ease;
|
|
}
|
|
.qmc-qcard:hover {
|
|
border-color: rgba(1,105,111,0.2);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
}
|
|
.qmc-qcard-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
|
.qmc-qcard-name-wrap { min-width: 0; }
|
|
.qmc-qcard-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.qmc-qcard-meta { font-size: 11px; color: #8a8a86; margin-top: 1px; }
|
|
.qmc-qcard-bar-row { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
|
|
.qmc-qcard-bar-label { font-size: 10px; font-weight: 600; color: #8a8a86; min-width: 28px; }
|
|
.qmc-qcard-bar-track {
|
|
flex: 1;
|
|
height: 4px;
|
|
background: rgba(0,0,0,0.06);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
.qmc-qcard-bar-fill {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.qmc-qcard-tags { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
|
|
.qmc-empty-col {
|
|
padding: 32px 0;
|
|
text-align: center;
|
|
font-size: 11px;
|
|
color: #8a8a86;
|
|
border: 1px dashed rgba(0,0,0,0.08);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* ── Pipeline grid (5 columns) ── */
|
|
.qmc-pipeline-scroll { overflow-x: auto; margin: 0 -4px; padding: 0 4px 8px; }
|
|
.qmc-pipeline-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 12px;
|
|
min-width: 60rem;
|
|
}
|
|
.qmc-pcol {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-radius: 12px;
|
|
background: var(--page-bg, #fafafa);
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
padding: 12px;
|
|
}
|
|
.qmc-pcol-head { display: flex; align-items: center; gap: 6px; }
|
|
.qmc-pcol-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
.qmc-pcol-label { font-size: 13px; font-weight: 600; color: var(--text-primary); }
|
|
.qmc-pcol-count {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #8a8a86;
|
|
margin-left: auto;
|
|
}
|
|
.qmc-pcol-hint { font-size: 10px; color: #8a8a86; margin-top: 2px; margin-bottom: 10px; }
|
|
.qmc-pcol-cards { display: flex; flex-direction: column; gap: 8px; flex: 1; }
|
|
|
|
/* ── Section / card (analytics) ── */
|
|
.qmc-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);
|
|
}
|
|
.qmc-section { padding: 20px 24px; }
|
|
.qmc-section-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin-bottom: 16px; }
|
|
.qmc-section-sub { font-size: 13px; color: #8a8a86; margin-top: -10px; margin-bottom: 14px; }
|
|
.qmc-section-eyebrow {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* ── LOB grid ── */
|
|
.qmc-lob-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
.qmc-lob-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 14px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff;
|
|
transition: all 150ms ease;
|
|
}
|
|
.qmc-lob-card:hover {
|
|
border-color: rgba(1,105,111,0.25);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
}
|
|
.qmc-lob-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: rgba(1,105,111,0.06);
|
|
color: #01696f;
|
|
flex-shrink: 0;
|
|
}
|
|
.qmc-lob-name { font-size: 13px; font-weight: 500; color: var(--text-primary); flex: 1; }
|
|
.qmc-lob-arrow { width: 14px; height: 14px; color: #8a8a86; opacity: 0; transition: opacity 150ms ease; }
|
|
.qmc-lob-card:hover .qmc-lob-arrow { opacity: 1; }
|
|
|
|
/* ── Analytics KPI strip ── */
|
|
.qmc-analytics-kpis {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 12px;
|
|
}
|
|
.qmc-akpi {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 16px 12px;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.qmc-akpi-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
text-align: center;
|
|
}
|
|
.qmc-akpi-value { font-size: 22px; font-weight: 700; color: var(--text-primary); }
|
|
.qmc-akpi-value--brand { color: #01696f; }
|
|
|
|
/* ── Funnel ── */
|
|
.qmc-funnel { display: flex; flex-direction: column; gap: 8px; }
|
|
.qmc-funnel-row {
|
|
display: grid;
|
|
grid-template-columns: 150px 1fr 36px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.qmc-funnel-label {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.qmc-funnel-label--waiting { color: #8a8a86; font-style: italic; }
|
|
.qmc-funnel-wait-dot { width: 5px; height: 5px; border-radius: 50%; background: #f59e0b; flex-shrink: 0; }
|
|
.qmc-funnel-bar-track {
|
|
height: 22px;
|
|
background: rgba(0,0,0,0.03);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.qmc-funnel-bar {
|
|
height: 100%;
|
|
border-radius: 6px;
|
|
transition: width 0.4s ease;
|
|
min-width: 4px;
|
|
}
|
|
.qmc-funnel-count { font-size: 14px; font-weight: 600; text-align: right; color: var(--text-primary); }
|
|
|
|
/* ── Agent performance ── */
|
|
.qmc-agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
|
.qmc-agent-card {
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
background: var(--page-bg, #fafafa);
|
|
}
|
|
.qmc-agent-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
|
.qmc-agent-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #01696f, #0d8a8f);
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
.qmc-agent-name { font-size: 13px; font-weight: 600; color: var(--text-primary); }
|
|
.qmc-agent-sub { font-size: 11px; color: #8a8a86; }
|
|
.qmc-agent-stats { display: flex; flex-direction: column; gap: 6px; }
|
|
.qmc-agent-stat { display: flex; align-items: center; justify-content: space-between; }
|
|
.qmc-agent-stat-label { font-size: 11px; color: #8a8a86; }
|
|
.qmc-agent-stat-badge {
|
|
display: inline-flex;
|
|
padding: 2px 8px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.qmc-agent-stat-val { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
|
|
/* ── Stuck deals ── */
|
|
.qmc-stuck-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.qmc-stuck-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.04);
|
|
border-left: 3px solid;
|
|
}
|
|
.qmc-stuck-info { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
|
.qmc-stuck-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
|
.qmc-stuck-meta { font-size: 12px; color: #8a8a86; }
|
|
.qmc-stuck-days { font-weight: 600; }
|
|
.qmc-stuck-agent { font-size: 12px; color: #8a8a86; min-width: 120px; }
|
|
.qmc-btn-nudge {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
padding: 4px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
background: #fff;
|
|
color: #01696f;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
.qmc-btn-nudge:hover { background: rgba(1,105,111,0.06); }
|
|
|
|
/* ── Stage tabs ── */
|
|
.qmc-stage-tabs {
|
|
display: inline-flex;
|
|
background: rgba(0,0,0,0.04);
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
gap: 2px;
|
|
margin-bottom: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* ── Stage breakdown list ── */
|
|
.qmc-stage-list { display: flex; flex-direction: column; gap: 4px; }
|
|
.qmc-stage-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 40px 120px 80px 140px;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
font-size: 13px;
|
|
}
|
|
.qmc-stage-row:last-child { border-bottom: none; }
|
|
.qmc-stage-row-main { display: flex; flex-direction: column; }
|
|
.qmc-stage-row-name { font-weight: 500; color: var(--text-primary); }
|
|
.qmc-stage-row-line { font-size: 11px; color: #8a8a86; }
|
|
.qmc-stage-row-days { font-weight: 600; text-align: center; }
|
|
.qmc-stage-row-agent { font-size: 12px; color: #8a8a86; }
|
|
.qmc-stage-row-prem { font-size: 12px; font-weight: 500; color: var(--text-primary); text-align: right; }
|
|
.qmc-stage-row-bar { display: flex; align-items: center; gap: 6px; }
|
|
.qmc-stage-bar-track {
|
|
flex: 1;
|
|
height: 5px;
|
|
background: rgba(0,0,0,0.06);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
.qmc-stage-bar-fill {
|
|
height: 100%;
|
|
background: #01696f;
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.qmc-stage-bar-pct { font-size: 11px; color: #8a8a86; min-width: 28px; }
|
|
|
|
/* ── Empty ── */
|
|
.qmc-empty-state { font-size: 13px; color: #8a8a86; padding: 24px 0; text-align: center; }
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 1024px) {
|
|
.qmc-kpi-grid { grid-template-columns: 1fr 1fr; }
|
|
.qmc-tracker-grid { grid-template-columns: 1fr; }
|
|
.qmc-analytics-kpis { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
@media (max-width: 640px) {
|
|
.qmc-kpi-grid { grid-template-columns: 1fr; }
|
|
.qmc-analytics-kpis { grid-template-columns: repeat(2, 1fr); }
|
|
.qmc-pipeline-grid { min-width: 50rem; }
|
|
.qmc-stage-row { grid-template-columns: 1fr; gap: 4px; }
|
|
}
|
|
</style>
|