WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

994
app/pages/quotes/new.vue Normal file
View File

@@ -0,0 +1,994 @@
<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>