2290 lines
68 KiB
Vue
2290 lines
68 KiB
Vue
<script setup lang="ts">
|
|
import { MOCK_CUSTOMERS, MOCK_CUSTOMERS_BY_ID, fmtMoney, type MockCustomer, type MockPolicy } from '~/data/mock-customers'
|
|
import { customerTier } from '~/data/mock-customers'
|
|
|
|
definePageMeta({ ssr: false })
|
|
|
|
const route = useRoute()
|
|
const id = route.params.id as string
|
|
|
|
const { data, error, pending, refresh } = useCustomer(`/customers/${id}`)
|
|
const customer = computed(() => data.value?.data)
|
|
|
|
const isIndividual = computed(() => customer.value?.customer_type !== 'corporate')
|
|
|
|
const { isFavorite, toggleFavorite } = useClientFavorites()
|
|
const { getTierForCustomer, getScoreForCustomer, config: attentionConfig } = useCustomerAttention()
|
|
|
|
/* ── Resolve mock client by route id ── */
|
|
const mock = computed<MockCustomer>(() => {
|
|
return MOCK_CUSTOMERS_BY_ID[id] ?? MOCK_CUSTOMERS[0]!
|
|
})
|
|
|
|
usePageTitle(computed(() => mock.value.name))
|
|
|
|
const customerName = computed(() => {
|
|
if (!customer.value) return mock.value.name || 'Unnamed customer'
|
|
if (isIndividual.value) {
|
|
const full = [customer.value.first_name, customer.value.last_name].filter(Boolean).join(' ')
|
|
return full || 'Unnamed customer'
|
|
}
|
|
return customer.value.commercial_name || customer.value.legal_name || 'Unnamed company'
|
|
})
|
|
|
|
const policyFilterValue = computed(() =>
|
|
isIndividual.value ? customer.value?.document_id : customer.value?.ruc
|
|
)
|
|
const { data: policiesData } = usePolicy('/policies', {
|
|
query: computed(() => policyFilterValue.value
|
|
? {
|
|
'filters[0][field]': 'search',
|
|
'filters[0][op]': '==',
|
|
'filters[0][value]': policyFilterValue.value
|
|
}
|
|
: {})
|
|
})
|
|
const customerPolicies = computed(() => policiesData.value?.data ?? [])
|
|
|
|
const activeTab = ref<'policies' | 'claims' | 'payments' | 'activity' | 'history' | 'relationships' | 'notes'>('policies')
|
|
|
|
const totalPremium = computed(() => {
|
|
const sum = mock.value.policies.reduce((s, p) => s + p.premium, 0)
|
|
return fmtMoney(sum)
|
|
})
|
|
|
|
const formatDate = (date: string) => {
|
|
if (!date) return '—'
|
|
return new Date(date).toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' })
|
|
}
|
|
|
|
const activityDotColor: Record<string, string> = {
|
|
claim: '#c13838',
|
|
payment: '#0f7b5f',
|
|
renewal: '#7c3aed',
|
|
quote: '#01696f',
|
|
note: '#8a8a86',
|
|
policy: '#0d5c63',
|
|
onboarding: '#01696f',
|
|
}
|
|
|
|
const activityIcon: Record<string, string> = {
|
|
claim: 'i-heroicons-exclamation-triangle',
|
|
payment: 'i-heroicons-banknotes',
|
|
renewal: 'i-heroicons-arrow-path',
|
|
quote: 'i-heroicons-document-text',
|
|
note: 'i-heroicons-chat-bubble-left-ellipsis',
|
|
policy: 'i-heroicons-shield-check',
|
|
onboarding: 'i-heroicons-user-plus',
|
|
}
|
|
|
|
/* ── Tier ── */
|
|
const tier = computed(() => customerTier(mock.value))
|
|
|
|
const tierLabel: Record<string, string> = {
|
|
quick_lead: 'Quick Lead',
|
|
lead: 'Lead',
|
|
customer: 'Customer',
|
|
cancelled: 'Cancelled',
|
|
}
|
|
const tierClass: Record<string, string> = {
|
|
quick_lead: 'cp-tier-quick-lead',
|
|
lead: 'cp-tier-lead',
|
|
customer: 'cp-tier-customer',
|
|
cancelled: 'cp-tier-cancelled',
|
|
}
|
|
|
|
/* ── Service grade from attention config ── */
|
|
const customerGradeInput = computed(() => ({
|
|
totalPremium: mock.value.policies.reduce((s, p) => s + p.premium, 0),
|
|
policyCount: mock.value.policies.length,
|
|
lineCount: [...new Set(mock.value.policies.filter(p => p.status === 'Active').map(p => p.line))].length,
|
|
tenureYears: Math.floor((Date.now() - new Date(mock.value.since).getTime()) / (365.25 * 24 * 60 * 60 * 1000)),
|
|
isCollectivoMember: false,
|
|
hasPrivatePolicies: mock.value.policies.some(p => p.status === 'Active'),
|
|
estimatedCommission: mock.value.policies.reduce((s, p) => s + p.premium, 0) * 0.15,
|
|
}))
|
|
const serviceGrade = computed(() => getTierForCustomer(customerGradeInput.value))
|
|
const serviceScore = computed(() => getScoreForCustomer(customerGradeInput.value))
|
|
|
|
/* ── Upcoming events ── */
|
|
const upcomingEvents = computed(() => {
|
|
const events: { label: string; date: string; type: string; icon: string }[] = []
|
|
const now = new Date()
|
|
// Upcoming renewals
|
|
for (const pol of mock.value.policies.filter(p => p.status === 'Active')) {
|
|
const renewal = new Date(pol.renewal)
|
|
if (renewal > now) {
|
|
const days = Math.ceil((renewal.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
|
events.push({
|
|
label: `${pol.line} renewal — ${pol.carrier}`,
|
|
date: days <= 30 ? `${days}d` : formatDate(pol.renewal),
|
|
type: days <= 30 ? 'urgent' : 'upcoming',
|
|
icon: 'i-heroicons-arrow-path',
|
|
})
|
|
}
|
|
}
|
|
// Open claims
|
|
for (const cl of openClaims.value) {
|
|
events.push({
|
|
label: `Claim ${cl.id} — ${cl.type}`,
|
|
date: cl.status,
|
|
type: 'action',
|
|
icon: 'i-heroicons-exclamation-triangle',
|
|
})
|
|
}
|
|
// Pending payments
|
|
for (const pay of mock.value.payments.filter(p => p.status === 'Pending' || p.status === 'Overdue')) {
|
|
events.push({
|
|
label: `Payment due — ${pay.policy}`,
|
|
date: pay.status,
|
|
type: pay.status === 'Overdue' ? 'urgent' : 'action',
|
|
icon: 'i-heroicons-banknotes',
|
|
})
|
|
}
|
|
return events.slice(0, 5)
|
|
})
|
|
|
|
/* ── Account Orientation ── */
|
|
const ALL_LINES = ['Auto', 'Home', 'Life', 'Health', 'Umbrella', 'Renter'] as const
|
|
|
|
const coveredLines = computed(() =>
|
|
[...new Set(mock.value.policies.filter(p => p.status === 'Active').map(p => p.line))]
|
|
)
|
|
|
|
const coverageGaps = computed(() =>
|
|
ALL_LINES.filter(l => !coveredLines.value.includes(l))
|
|
)
|
|
|
|
const riskProfile = computed(() => {
|
|
const openClaims = mock.value.claims.filter(c => c.status !== 'Resolved' && c.status !== 'Denied').length
|
|
if (openClaims >= 2) return 'High'
|
|
if (openClaims === 1) return 'Medium'
|
|
return 'Low'
|
|
})
|
|
|
|
const nextRenewal = computed(() => {
|
|
const now = new Date()
|
|
const upcoming = mock.value.policies
|
|
.filter(p => p.status === 'Active' && new Date(p.renewal) > now)
|
|
.sort((a, b) => new Date(a.renewal).getTime() - new Date(b.renewal).getTime())
|
|
return upcoming.length > 0 ? formatDate(upcoming[0]!.renewal) : '—'
|
|
})
|
|
|
|
/* ── KPI helpers ── */
|
|
const activePolicies = computed(() => mock.value.policies.filter(p => p.status === 'Active'))
|
|
const openClaims = computed(() => mock.value.claims.filter(c => c.status !== 'Resolved' && c.status !== 'Denied'))
|
|
const yearsSince = computed(() => Math.floor((Date.now() - new Date(mock.value.since).getTime()) / (365.25 * 24 * 60 * 60 * 1000)))
|
|
|
|
/* ── Service Actions ── */
|
|
const lastThreeActivity = computed(() => mock.value.activity.slice(0, 3))
|
|
|
|
const openActions = computed(() => {
|
|
const items: { icon: string; text: string; status: string; statusClass: string }[] = []
|
|
for (const claim of openClaims.value) {
|
|
items.push({
|
|
icon: 'i-heroicons-exclamation-triangle',
|
|
text: `${claim.id} — ${claim.type} ($${claim.amount.toLocaleString()})`,
|
|
status: claim.status,
|
|
statusClass: claim.status === 'In progress' ? 'cp-status-warn' : claim.status === 'Under review' ? 'cp-status-pending' : 'cp-status-ok',
|
|
})
|
|
}
|
|
for (const pay of mock.value.payments.filter(p => p.status === 'Pending' || p.status === 'Overdue' || p.status === 'Failed')) {
|
|
items.push({
|
|
icon: 'i-heroicons-banknotes',
|
|
text: `${pay.status} payment — ${pay.policy} ($${pay.amount.toLocaleString()})`,
|
|
status: pay.status,
|
|
statusClass: pay.status === 'Failed' ? 'cp-status-bad' : pay.status === 'Overdue' ? 'cp-status-warn' : 'cp-status-pending',
|
|
})
|
|
}
|
|
return items
|
|
})
|
|
|
|
const recentlyClosed = computed(() => {
|
|
const items: { icon: string; text: string; date: string }[] = []
|
|
for (const claim of mock.value.claims.filter(c => c.status === 'Resolved')) {
|
|
items.push({
|
|
icon: 'i-heroicons-check-circle',
|
|
text: `${claim.id} — ${claim.type} resolved ($${claim.amount.toLocaleString()})`,
|
|
date: formatDate(claim.date),
|
|
})
|
|
}
|
|
for (const pay of mock.value.payments.filter(p => p.status === 'Paid').slice(0, 3)) {
|
|
items.push({
|
|
icon: 'i-heroicons-check-circle',
|
|
text: `Payment — ${pay.policy} ($${pay.amount.toLocaleString()})`,
|
|
date: formatDate(pay.date),
|
|
})
|
|
}
|
|
return items.slice(0, 3)
|
|
})
|
|
|
|
/* ── Policies grouped by status ── */
|
|
const activePoliciesGroup = computed(() => mock.value.policies.filter(p => p.status === 'Active'))
|
|
const pendingPoliciesGroup = computed(() => mock.value.policies.filter(p => p.status === 'Pending'))
|
|
const historicalPolicies = computed(() => mock.value.policies.filter(p => p.status === 'Lapsed' || p.status === 'Cancelled'))
|
|
const showHistorical = ref(false)
|
|
|
|
/* ── Relationships (mock for cust-002) ── */
|
|
const hasRelationship = computed(() => id === 'cust-002')
|
|
|
|
/* ── Sales agent assignment ── */
|
|
const AGENTS = [
|
|
{ id: 'AG-001', name: 'Ana Ramírez', role: 'Senior producer' },
|
|
{ id: 'AG-002', name: 'Marco Villanueva', role: 'Producer' },
|
|
{ id: 'AG-003', name: 'Lucía Fernández', role: 'Junior producer' },
|
|
{ id: 'AG-004', name: 'Carlos Méndez', role: 'Producer (inactive)' },
|
|
]
|
|
const agentItems = AGENTS.map(a => ({ label: `${a.name} — ${a.role}`, value: a.id }))
|
|
// Seed: map mock.agent display name to agent ID
|
|
const defaultAgentId = computed(() => {
|
|
if (mock.value.agent === 'Ana R.') return 'AG-001'
|
|
if (mock.value.agent === 'Marco V.') return 'AG-002'
|
|
return null
|
|
})
|
|
const assignedAgentId = ref<string | null>(null)
|
|
// Initialize on mount
|
|
onMounted(() => { assignedAgentId.value = defaultAgentId.value })
|
|
const assignedAgentName = computed(() => {
|
|
const a = AGENTS.find(ag => ag.id === assignedAgentId.value)
|
|
return a ? a.name : '—'
|
|
})
|
|
|
|
const toast = useToast()
|
|
function onAgentChange(agentId: string | null) {
|
|
assignedAgentId.value = agentId
|
|
const a = AGENTS.find(ag => ag.id === agentId)
|
|
if (a) toast.add({ title: `Sales agent assigned: ${a.name}`, color: 'success' })
|
|
}
|
|
function onPolicyClick(pol: MockPolicy) {
|
|
navigateTo(`/policies/${pol.id}`)
|
|
}
|
|
function onClaimClick(claimId: string) {
|
|
navigateTo(`/claims/${claimId}`)
|
|
}
|
|
|
|
/* ── Full audit history ── */
|
|
const auditHistory = computed(() => {
|
|
const entries: { date: string; sortDate: number; action: string; actor: string; detail: string; type: string }[] = []
|
|
// Activity events
|
|
for (const ev of mock.value.activity) {
|
|
entries.push({
|
|
date: ev.date,
|
|
sortDate: new Date(ev.date).getTime() || 0,
|
|
action: ev.text,
|
|
actor: mock.value.agent,
|
|
detail: '',
|
|
type: ev.type,
|
|
})
|
|
}
|
|
// Claims
|
|
for (const cl of mock.value.claims) {
|
|
entries.push({
|
|
date: formatDate(cl.date),
|
|
sortDate: new Date(cl.date).getTime(),
|
|
action: `Claim ${cl.id} filed — ${cl.type}`,
|
|
actor: mock.value.agent,
|
|
detail: `Policy ${cl.policy} · $${cl.amount.toLocaleString()} · Status: ${cl.status}`,
|
|
type: 'claim',
|
|
})
|
|
}
|
|
// Payments
|
|
for (const pay of mock.value.payments) {
|
|
entries.push({
|
|
date: formatDate(pay.date),
|
|
sortDate: new Date(pay.date).getTime(),
|
|
action: `Payment ${pay.status.toLowerCase()} — $${pay.amount.toLocaleString()}`,
|
|
actor: 'System',
|
|
detail: `Policy ${pay.policy} · ${pay.method}`,
|
|
type: 'payment',
|
|
})
|
|
}
|
|
// Policies
|
|
for (const pol of mock.value.policies) {
|
|
entries.push({
|
|
date: formatDate(pol.renewal),
|
|
sortDate: new Date(pol.renewal).getTime(),
|
|
action: `Policy ${pol.id} — ${pol.status === 'Active' ? 'Active' : pol.status}`,
|
|
actor: mock.value.agent,
|
|
detail: `${pol.carrier} · ${pol.product} · $${pol.premium.toLocaleString()}/yr`,
|
|
type: 'policy',
|
|
})
|
|
}
|
|
// System events
|
|
entries.push({
|
|
date: formatDate(mock.value.since),
|
|
sortDate: new Date(mock.value.since).getTime(),
|
|
action: 'Customer account created',
|
|
actor: 'System',
|
|
detail: `Type: ${mock.value.type} · Document: ${mock.value.documentId}`,
|
|
type: 'onboarding',
|
|
})
|
|
entries.push({
|
|
date: 'Today',
|
|
sortDate: Date.now(),
|
|
action: `Sales agent assigned: ${assignedAgentName.value}`,
|
|
actor: 'System',
|
|
detail: 'Agent of record for this customer',
|
|
type: 'note',
|
|
})
|
|
return entries.sort((a, b) => b.sortDate - a.sortDate)
|
|
})
|
|
|
|
const historyFilter = ref<'all' | 'claim' | 'payment' | 'policy' | 'note' | 'onboarding'>('all')
|
|
const filteredHistory = computed(() => {
|
|
if (historyFilter.value === 'all') return auditHistory.value
|
|
return auditHistory.value.filter(e => e.type === historyFilter.value)
|
|
})
|
|
|
|
/* ── Notes mock data ── */
|
|
const mockNotes = [
|
|
{ id: 1, text: 'Client prefers phone calls over email for policy updates. Best time to reach: mornings before 11 AM.', author: mock.value.agent, date: 'Mar 15, 2025', pinned: true },
|
|
{ id: 2, text: 'Discussed increasing umbrella coverage at next renewal. Client is interested in reviewing options.', author: mock.value.agent, date: 'Feb 20, 2025', pinned: true },
|
|
{ id: 3, text: 'Annual review completed. All policies in good standing. Consider cross-sell opportunity for health insurance.', author: 'System', date: 'Jan 10, 2025', pinned: false },
|
|
]
|
|
const newNote = ref('')
|
|
|
|
/* ── Documents mock ── */
|
|
const documentCategories = [
|
|
{ name: 'Policy Documents', count: 3, icon: 'i-heroicons-document-text' },
|
|
{ name: 'ID & KYC', count: 2, icon: 'i-heroicons-identification' },
|
|
{ name: 'Claims', count: mock.value.claims.length > 0 ? 1 : 0, icon: 'i-heroicons-clipboard-document-check' },
|
|
{ name: 'Correspondence', count: 0, icon: 'i-heroicons-envelope' },
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<div class="cp-page mx-auto max-w-6xl pb-12">
|
|
<!-- ═══ BACK ═══ -->
|
|
<NuxtLink to="/customers" class="inline-flex">
|
|
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Customers</UButton>
|
|
</NuxtLink>
|
|
|
|
<!-- ═══ HEADER ═══ -->
|
|
<div class="cp-header">
|
|
<div class="cp-header-left">
|
|
<div class="cp-avatar">{{ mock.initials }}</div>
|
|
<div class="cp-header-info">
|
|
<div class="cp-header-name-row">
|
|
<h1 class="cp-name">{{ customerName }}</h1>
|
|
<span class="cp-type-badge">{{ mock.type }}</span>
|
|
<span class="cp-tier-badge" :class="tierClass[tier]">{{ tierLabel[tier] }}</span>
|
|
<span v-for="tag in mock.tags" :key="tag" class="cp-tag">{{ tag }}</span>
|
|
</div>
|
|
<div class="cp-header-meta">
|
|
<span>{{ mock.documentId }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ mock.email || '—' }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ mock.phone }}</span>
|
|
<span class="cp-sep" />
|
|
<span>Agent: {{ mock.agent }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="cp-header-actions">
|
|
<UButton
|
|
size="sm"
|
|
color="neutral"
|
|
variant="ghost"
|
|
:icon="isFavorite(mock.id) ? 'i-heroicons-star-solid' : 'i-heroicons-star'"
|
|
:style="isFavorite(mock.id) ? 'color: #d4a017;' : ''"
|
|
@click="toggleFavorite(mock.id)"
|
|
/>
|
|
<UButton size="sm" color="neutral" variant="outline" icon="i-heroicons-pencil-square">Edit</UButton>
|
|
<UButton size="sm" color="primary" icon="i-heroicons-document-text">New Quote</UButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ ACCOUNT ORIENTATION ═══ -->
|
|
<div class="cp-orient-card">
|
|
<!-- Left: Grade + Status summary -->
|
|
<div class="cp-orient-summary">
|
|
<div class="cp-grade-badge" :style="`background: ${serviceGrade.color}12; border-color: ${serviceGrade.color}30;`">
|
|
<UIcon :name="serviceGrade.icon" :style="`color: ${serviceGrade.color}; width: 20px; height: 20px;`" />
|
|
<div>
|
|
<p class="cp-grade-name" :style="`color: ${serviceGrade.color}`">{{ serviceGrade.name }}</p>
|
|
<p class="cp-grade-score">{{ serviceScore }} pts</p>
|
|
</div>
|
|
</div>
|
|
<div class="cp-orient-facts">
|
|
<div class="cp-orient-fact">
|
|
<span class="cp-orient-fact-label">Status</span>
|
|
<span class="cp-orient-fact-value">
|
|
<span class="cp-orient-dot" :class="activePolicies.length > 0 ? 'cp-dot-green' : 'cp-dot-gray'" />
|
|
{{ activePolicies.length > 0 ? 'Active' : 'Inactive' }}
|
|
</span>
|
|
</div>
|
|
<div class="cp-orient-fact">
|
|
<span class="cp-orient-fact-label">Preferred Contact</span>
|
|
<span class="cp-orient-fact-value">Phone</span>
|
|
</div>
|
|
<div class="cp-orient-fact">
|
|
<span class="cp-orient-fact-label">Language</span>
|
|
<span class="cp-orient-fact-value">{{ mock.preferredLang }}</span>
|
|
</div>
|
|
<div class="cp-orient-fact">
|
|
<span class="cp-orient-fact-label">Coverage</span>
|
|
<span class="cp-orient-fact-value">
|
|
{{ coveredLines.length }} of {{ ALL_LINES.length }} lines
|
|
<template v-if="coverageGaps.length > 0 && coverageGaps.length < ALL_LINES.length">
|
|
<span v-for="gap in coverageGaps" :key="gap" class="cp-gap-chip">{{ gap }}</span>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Upcoming events -->
|
|
<div class="cp-orient-events">
|
|
<p class="cp-orient-events-title">
|
|
<UIcon name="i-heroicons-calendar-days" style="width: 14px; height: 14px; opacity: 0.5;" />
|
|
Upcoming
|
|
</p>
|
|
<div v-if="upcomingEvents.length > 0" class="cp-events-list">
|
|
<div
|
|
v-for="(ev, i) in upcomingEvents"
|
|
:key="i"
|
|
class="cp-event-item"
|
|
:class="[`cp-event-${ev.type}`, ev.icon === 'i-heroicons-arrow-path' ? 'cp-event-clickable' : '']"
|
|
@click="ev.icon === 'i-heroicons-arrow-path' ? navigateTo('/renewals') : ev.icon === 'i-heroicons-exclamation-triangle' ? navigateTo('/claims') : null"
|
|
>
|
|
<UIcon :name="ev.icon" style="width: 13px; height: 13px; flex-shrink: 0; margin-top: 1px;" />
|
|
<span class="cp-event-label">{{ ev.label }}</span>
|
|
<span class="cp-event-date">{{ ev.date }}</span>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-orient-quiet">Nothing upcoming — all clear</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ KPI STRIP ═══ -->
|
|
<div class="cp-kpi-strip">
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Total Premium</p>
|
|
<p class="cp-kpi-value">{{ totalPremium }}</p>
|
|
<p class="cp-kpi-sub">/year across {{ mock.policies.length }} {{ mock.policies.length === 1 ? 'policy' : 'policies' }}</p>
|
|
</div>
|
|
<div class="cp-kpi-div" />
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Active Policies</p>
|
|
<p class="cp-kpi-value">{{ activePolicies.length }}</p>
|
|
<p class="cp-kpi-sub">{{ coveredLines.join(', ') || 'None' }}</p>
|
|
</div>
|
|
<div class="cp-kpi-div" />
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Open Claims</p>
|
|
<p class="cp-kpi-value">{{ openClaims.length }}</p>
|
|
<p class="cp-kpi-sub">{{ openClaims.length > 0 ? openClaims.map(c => c.id).join(', ') : 'No open claims' }}</p>
|
|
</div>
|
|
<div class="cp-kpi-div" />
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Client Since</p>
|
|
<p class="cp-kpi-value">{{ mock.since.slice(0, 4) }}</p>
|
|
<p class="cp-kpi-sub">{{ yearsSince }}+ years{{ mock.tags.length ? ' \u00b7 ' + mock.tags[0] : '' }}</p>
|
|
</div>
|
|
<div class="cp-kpi-div" />
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Payment Status</p>
|
|
<p class="cp-kpi-value" :class="mock.paymentStatus === 'Current' ? 'cp-kpi-good' : mock.paymentStatus === 'Overdue' ? 'cp-kpi-bad' : mock.paymentStatus === 'N/A' ? '' : 'cp-kpi-warn'">{{ mock.paymentStatus }}</p>
|
|
<p class="cp-kpi-sub">{{ mock.paymentStatus === 'Current' ? 'All premiums up to date' : mock.paymentStatus === 'Overdue' ? 'Payment action required' : mock.paymentStatus === 'N/A' ? 'No active billing' : 'Payment pending' }}</p>
|
|
</div>
|
|
<div class="cp-kpi-div" />
|
|
<div class="cp-kpi">
|
|
<p class="cp-kpi-label">Coverage Lines</p>
|
|
<p class="cp-kpi-value">{{ coveredLines.length }} of {{ ALL_LINES.length }}</p>
|
|
<p class="cp-kpi-sub">{{ coveredLines.length === ALL_LINES.length ? 'Full coverage' : coverageGaps.length + ' gap' + (coverageGaps.length === 1 ? '' : 's') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ QUICK POLICY ACCESS ═══ -->
|
|
<div v-if="mock.policies.length > 0" class="cp-quick-policies">
|
|
<div class="cp-quick-pol-head">
|
|
<div class="cp-quick-pol-title">
|
|
<UIcon name="i-heroicons-document-arrow-down" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Policies</span>
|
|
<span class="cp-quick-pol-count">{{ mock.policies.filter(p => p.status === 'Active').length }} active</span>
|
|
</div>
|
|
<div class="cp-quick-pol-actions">
|
|
<UButton size="xs" color="neutral" variant="soft" icon="i-heroicons-printer">Print All</UButton>
|
|
<UButton size="xs" color="neutral" variant="soft" icon="i-heroicons-paper-airplane">Send to Client</UButton>
|
|
<UButton size="xs" color="primary" variant="soft" icon="i-heroicons-arrow-down-tray">Download All</UButton>
|
|
</div>
|
|
</div>
|
|
<div class="cp-quick-pol-grid">
|
|
<NuxtLink v-for="pol in mock.policies.filter(p => p.status === 'Active')" :key="pol.id" :to="`/policies/${pol.id}`" class="cp-quick-pol-chip" style="text-decoration: none; color: inherit;">
|
|
<div class="cp-quick-pol-icon" :style="`background: ${pol.line === 'Auto' ? 'rgba(1,105,111,0.08)' : pol.line === 'Home' ? 'rgba(194,123,26,0.08)' : pol.line === 'Life' ? 'rgba(124,58,237,0.08)' : pol.line === 'Health' ? 'rgba(15,123,95,0.08)' : 'rgba(0,0,0,0.04)'}`">
|
|
<UIcon :name="pol.icon" style="width: 16px; height: 16px;" />
|
|
</div>
|
|
<div class="cp-quick-pol-info">
|
|
<p class="cp-quick-pol-name">{{ pol.line }} — {{ pol.carrier }}</p>
|
|
<p class="cp-quick-pol-id">{{ pol.id }}</p>
|
|
</div>
|
|
<div class="cp-quick-pol-right">
|
|
<span class="cp-quick-pol-premium">${{ pol.premium.toLocaleString() }}/yr</span>
|
|
<button type="button" class="cp-quick-pol-dl" title="Download policy" @click.prevent>
|
|
<UIcon name="i-heroicons-bolt" style="width: 14px; height: 14px;" />
|
|
</button>
|
|
</div>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ SERVICE ACTIONS ═══ -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-clipboard-document-list" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Service Actions</span>
|
|
</div>
|
|
<div class="cp-service-grid">
|
|
<!-- Last 3 Service Actions -->
|
|
<div class="cp-service-section">
|
|
<p class="cp-service-title">Recent Activity</p>
|
|
<div v-if="lastThreeActivity.length > 0" class="cp-timeline">
|
|
<div v-for="(event, i) in lastThreeActivity" :key="i" class="cp-timeline-item">
|
|
<div class="cp-timeline-dot" :style="`background: ${activityDotColor[event.type] || '#8a8a86'}`" />
|
|
<div class="cp-timeline-content">
|
|
<p class="cp-timeline-text">{{ event.text }}</p>
|
|
<p class="cp-timeline-date">{{ event.date }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No recent activity</p>
|
|
</div>
|
|
|
|
<!-- Open Service Actions -->
|
|
<div class="cp-service-section">
|
|
<p class="cp-service-title">Open Actions</p>
|
|
<div v-if="openActions.length > 0" class="cp-action-list">
|
|
<div v-for="(action, i) in openActions" :key="i" class="cp-action-item">
|
|
<UIcon :name="action.icon" class="cp-action-icon" />
|
|
<span class="cp-action-text">{{ action.text }}</span>
|
|
<span class="cp-status" :class="action.statusClass">{{ action.status }}</span>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No open actions</p>
|
|
</div>
|
|
|
|
<!-- Recently Closed -->
|
|
<div class="cp-service-section">
|
|
<p class="cp-service-title">Recently Closed</p>
|
|
<div v-if="recentlyClosed.length > 0" class="cp-action-list">
|
|
<div v-for="(item, i) in recentlyClosed" :key="i" class="cp-action-item">
|
|
<UIcon :name="item.icon" class="cp-action-icon cp-icon-green" />
|
|
<span class="cp-action-text">{{ item.text }}</span>
|
|
<span class="cp-action-date">{{ item.date }}</span>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No recently closed items</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ MAIN GRID ═══ -->
|
|
<div class="cp-main-grid">
|
|
<!-- LEFT: Personal Details -->
|
|
<div class="cp-card cp-details-col">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-user" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Personal Details</span>
|
|
</div>
|
|
<div class="cp-detail-list">
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Full Name</span>
|
|
<span class="cp-detail-value">{{ mock.name }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Document ID</span>
|
|
<span class="cp-detail-value cp-mono">{{ mock.documentId }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Email</span>
|
|
<span class="cp-detail-value">{{ mock.email || '—' }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Phone</span>
|
|
<span class="cp-detail-value">{{ mock.phone }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Date of Birth</span>
|
|
<span class="cp-detail-value">{{ mock.birthDate ? formatDate(mock.birthDate) : '—' }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Gender</span>
|
|
<span class="cp-detail-value">{{ mock.gender || '—' }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Marital Status</span>
|
|
<span class="cp-detail-value">—</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Address</span>
|
|
<span class="cp-detail-value">{{ mock.address || '—' }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Occupation</span>
|
|
<span class="cp-detail-value">—</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Language</span>
|
|
<span class="cp-detail-value">{{ mock.preferredLang }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Assigned Agent</span>
|
|
<span class="cp-detail-value">{{ mock.agent }}</span>
|
|
</div>
|
|
<div class="cp-detail-row cp-detail-row-agent">
|
|
<span class="cp-detail-label">Sales Agent</span>
|
|
<div class="cp-agent-assign">
|
|
<USelect
|
|
:model-value="assignedAgentId"
|
|
:items="agentItems"
|
|
value-key="value"
|
|
label-key="label"
|
|
placeholder="Assign agent…"
|
|
class="cp-agent-select"
|
|
@update:model-value="onAgentChange"
|
|
/>
|
|
<NuxtLink v-if="assignedAgentId" to="/settings/agents" class="cp-agent-link">
|
|
<UIcon name="i-heroicons-arrow-top-right-on-square" style="width: 12px; height: 12px;" />
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Client Since</span>
|
|
<span class="cp-detail-value">{{ formatDate(mock.since) }}</span>
|
|
</div>
|
|
<div class="cp-detail-row">
|
|
<span class="cp-detail-label">Referral Source</span>
|
|
<span class="cp-detail-value">
|
|
<span v-if="mock.tags.find(t => t.includes('eferral'))" class="cp-orient-tag">
|
|
{{ mock.tags.find(t => t.includes('eferral')) }}
|
|
</span>
|
|
<span v-else>Direct</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: Tabbed content -->
|
|
<div class="cp-tabbed-col">
|
|
<!-- Tabs -->
|
|
<div class="cp-tabs">
|
|
<button
|
|
v-for="tab in [
|
|
{ id: 'policies' as const, label: 'Policies', count: mock.policies.length },
|
|
{ id: 'claims' as const, label: 'Claims', count: mock.claims.length },
|
|
{ id: 'payments' as const, label: 'Payments', count: mock.payments.length },
|
|
{ id: 'activity' as const, label: 'Activity', count: null },
|
|
{ id: 'history' as const, label: 'History', count: null },
|
|
{ id: 'relationships' as const, label: 'Relationships', count: null },
|
|
{ id: 'notes' as const, label: 'Notes', count: null },
|
|
]"
|
|
:key="tab.id"
|
|
type="button"
|
|
class="cp-tab"
|
|
:class="activeTab === tab.id ? 'cp-tab-on' : 'cp-tab-off'"
|
|
@click="activeTab = tab.id"
|
|
>
|
|
{{ tab.label }}
|
|
<span v-if="tab.count !== null" class="cp-tab-count" :class="activeTab === tab.id ? 'cp-tab-count-on' : ''">{{ tab.count }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── POLICIES TAB ── -->
|
|
<div v-if="activeTab === 'policies'" class="cp-tab-content">
|
|
<!-- Coverage summary bar -->
|
|
<div v-if="mock.policies.length > 0" class="cp-coverage-bar">
|
|
<span class="cp-coverage-label">Coverage:</span>
|
|
<span v-for="line in ALL_LINES" :key="line" class="cp-coverage-dot-wrap">
|
|
<span class="cp-coverage-dot" :class="coveredLines.includes(line) ? 'cp-coverage-active' : 'cp-coverage-inactive'" />
|
|
<span class="cp-coverage-line-label" :class="coveredLines.includes(line) ? '' : 'cp-coverage-line-missing'">{{ line }}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Coverage gap CTA -->
|
|
<div v-if="coverageGaps.length > 0" class="cp-gap-cta">
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-heroicons-shield-exclamation" style="width: 14px; height: 14px; color: #d97706;" />
|
|
<span class="text-[12px] font-medium text-[var(--text-primary)]">{{ coverageGaps.length }} missing coverage {{ coverageGaps.length === 1 ? 'line' : 'lines' }}:</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 mt-1.5 pl-5">
|
|
<NuxtLink
|
|
v-for="gap in coverageGaps"
|
|
:key="gap"
|
|
:to="`/quotes/new?line=${encodeURIComponent(gap)}&customer=${id}`"
|
|
class="cp-gap-quote-btn"
|
|
>
|
|
{{ gap }}
|
|
<UIcon name="i-heroicons-arrow-right" style="width: 11px; height: 11px;" />
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active policies -->
|
|
<div v-if="activePoliciesGroup.length > 0" class="cp-policy-group">
|
|
<div v-for="pol in activePoliciesGroup" :key="pol.id" class="cp-card cp-card-hover" @click="onPolicyClick(pol)">
|
|
<div class="cp-policy-row">
|
|
<div class="cp-pol-icon">
|
|
<UIcon :name="pol.icon" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="cp-pol-info">
|
|
<div class="cp-pol-name-row">
|
|
<p class="cp-pol-product">{{ pol.product }}</p>
|
|
<span class="cp-line-badge">{{ pol.line }}</span>
|
|
<span class="cp-status-active">{{ pol.status }}</span>
|
|
</div>
|
|
<div class="cp-pol-meta">
|
|
<span>{{ pol.id }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ pol.carrier }}</span>
|
|
<span class="cp-sep" />
|
|
<span>Renewal {{ formatDate(pol.renewal) }}</span>
|
|
</div>
|
|
<p v-if="pol.details" class="cp-pol-details">{{ pol.details }}</p>
|
|
</div>
|
|
<div class="cp-pol-premium">
|
|
<p class="cp-pol-amount">${{ pol.premium.toLocaleString() }}</p>
|
|
<p class="cp-pol-period">/year</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pending policies -->
|
|
<div v-if="pendingPoliciesGroup.length > 0" class="cp-policy-group">
|
|
<p class="cp-group-label">Pending</p>
|
|
<div v-for="pol in pendingPoliciesGroup" :key="pol.id" class="cp-card cp-card-hover" @click="onPolicyClick(pol)">
|
|
<div class="cp-policy-row">
|
|
<div class="cp-pol-icon">
|
|
<UIcon :name="pol.icon" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="cp-pol-info">
|
|
<div class="cp-pol-name-row">
|
|
<p class="cp-pol-product">{{ pol.product }}</p>
|
|
<span class="cp-line-badge">{{ pol.line }}</span>
|
|
<span class="cp-status-pending-badge">{{ pol.status }}</span>
|
|
</div>
|
|
<div class="cp-pol-meta">
|
|
<span>{{ pol.id }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ pol.carrier }}</span>
|
|
</div>
|
|
<p v-if="pol.details" class="cp-pol-details">{{ pol.details }}</p>
|
|
</div>
|
|
<div class="cp-pol-premium">
|
|
<p class="cp-pol-amount">${{ pol.premium.toLocaleString() }}</p>
|
|
<p class="cp-pol-period">/year</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Historical (collapsed) -->
|
|
<div v-if="historicalPolicies.length > 0" class="cp-policy-group">
|
|
<button type="button" class="cp-historical-toggle" @click="showHistorical = !showHistorical">
|
|
<UIcon :name="showHistorical ? 'i-heroicons-chevron-down' : 'i-heroicons-chevron-right'" style="width: 14px; height: 14px;" />
|
|
<span>Historical ({{ historicalPolicies.length }})</span>
|
|
</button>
|
|
<template v-if="showHistorical">
|
|
<div v-for="pol in historicalPolicies" :key="pol.id" class="cp-card cp-card-historical" style="cursor: pointer;" @click="onPolicyClick(pol)">
|
|
<div class="cp-policy-row">
|
|
<div class="cp-pol-icon cp-pol-icon-muted">
|
|
<UIcon :name="pol.icon" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="cp-pol-info">
|
|
<div class="cp-pol-name-row">
|
|
<p class="cp-pol-product">{{ pol.product }}</p>
|
|
<span class="cp-line-badge">{{ pol.line }}</span>
|
|
<span class="cp-status-cancelled">{{ pol.status }}</span>
|
|
</div>
|
|
<div class="cp-pol-meta">
|
|
<span>{{ pol.id }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ pol.carrier }}</span>
|
|
</div>
|
|
<p v-if="pol.details" class="cp-pol-details">{{ pol.details }}</p>
|
|
</div>
|
|
<div class="cp-pol-premium">
|
|
<p class="cp-pol-amount cp-text-muted">${{ pol.premium.toLocaleString() }}</p>
|
|
<p class="cp-pol-period">/year</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<p v-if="mock.policies.length === 0" class="cp-empty-msg">No policies on file</p>
|
|
</div>
|
|
|
|
<!-- ── CLAIMS TAB ── -->
|
|
<div v-if="activeTab === 'claims'" class="cp-tab-content">
|
|
<div v-if="mock.claims.length > 0" class="cp-card">
|
|
<div v-for="(claim, i) in mock.claims" :key="claim.id" class="cp-row" :class="i < mock.claims.length - 1 ? 'cp-row-border' : ''" style="cursor: pointer;" @click="onClaimClick(claim.id)">
|
|
<div class="cp-row-main">
|
|
<div class="cp-row-top">
|
|
<p class="cp-row-title">{{ claim.id }} — {{ claim.type }}</p>
|
|
<span class="cp-status" :class="claim.status === 'In progress' ? 'cp-status-warn' : claim.status === 'Under review' ? 'cp-status-pending' : claim.status === 'Denied' ? 'cp-status-bad' : 'cp-status-ok'">{{ claim.status }}</span>
|
|
</div>
|
|
<p class="cp-row-sub">Policy {{ claim.policy }} · Filed {{ formatDate(claim.date) }}</p>
|
|
</div>
|
|
<p class="cp-row-amount">${{ claim.amount.toLocaleString() }}</p>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No claims on file</p>
|
|
</div>
|
|
|
|
<!-- ── PAYMENTS TAB ── -->
|
|
<div v-if="activeTab === 'payments'" class="cp-tab-content">
|
|
<div v-if="mock.payments.length > 0" class="cp-card">
|
|
<table class="cp-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Policy</th>
|
|
<th>Method</th>
|
|
<th>Status</th>
|
|
<th class="cp-text-right">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="pay in mock.payments" :key="pay.date + pay.policy">
|
|
<td class="cp-tabular">{{ formatDate(pay.date) }}</td>
|
|
<td class="cp-mono cp-text-xs">{{ pay.policy }}</td>
|
|
<td>{{ pay.method }}</td>
|
|
<td>
|
|
<span class="cp-status" :class="pay.status === 'Paid' ? 'cp-status-ok' : pay.status === 'Failed' ? 'cp-status-bad' : pay.status === 'Overdue' ? 'cp-status-warn' : 'cp-status-pending'">
|
|
{{ pay.status }}
|
|
</span>
|
|
</td>
|
|
<td class="cp-text-right cp-tabular cp-text-medium">${{ pay.amount.toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No payment history</p>
|
|
</div>
|
|
|
|
<!-- ── ACTIVITY TAB ── -->
|
|
<div v-if="activeTab === 'activity'" class="cp-tab-content">
|
|
<div v-if="mock.activity.length > 0" class="cp-card">
|
|
<div v-for="(event, i) in mock.activity" :key="i" class="cp-row" :class="i < mock.activity.length - 1 ? 'cp-row-border' : ''">
|
|
<div class="cp-activity-dot" :style="`background: ${activityDotColor[event.type] || '#8a8a86'}`" />
|
|
<UIcon :name="activityIcon[event.type] || 'i-heroicons-ellipsis-horizontal'" class="cp-activity-icon" :style="`color: ${activityDotColor[event.type] || '#8a8a86'}`" />
|
|
<div class="cp-row-main">
|
|
<p class="cp-row-text">{{ event.text }}</p>
|
|
</div>
|
|
<span class="cp-row-date">{{ event.date }}</span>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg">No activity recorded</p>
|
|
</div>
|
|
|
|
<!-- ── HISTORY TAB ── -->
|
|
<div v-if="activeTab === 'history'" class="cp-tab-content">
|
|
<!-- Filter bar -->
|
|
<div class="cp-history-filters">
|
|
<button
|
|
v-for="f in [
|
|
{ id: 'all' as const, label: 'All' },
|
|
{ id: 'claim' as const, label: 'Claims' },
|
|
{ id: 'payment' as const, label: 'Payments' },
|
|
{ id: 'policy' as const, label: 'Policies' },
|
|
{ id: 'note' as const, label: 'Notes' },
|
|
{ id: 'onboarding' as const, label: 'System' },
|
|
]"
|
|
:key="f.id"
|
|
type="button"
|
|
class="cp-history-filter"
|
|
:class="historyFilter === f.id ? 'cp-history-filter-on' : 'cp-history-filter-off'"
|
|
@click="historyFilter = f.id"
|
|
>
|
|
{{ f.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="cp-card">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-clock" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Audit Log</span>
|
|
<span class="cp-history-count">{{ filteredHistory.length }} entries</span>
|
|
</div>
|
|
<div v-if="filteredHistory.length > 0">
|
|
<div
|
|
v-for="(entry, i) in filteredHistory"
|
|
:key="i"
|
|
class="cp-history-row"
|
|
:class="i < filteredHistory.length - 1 ? 'cp-row-border' : ''"
|
|
>
|
|
<div class="cp-history-dot-col">
|
|
<div class="cp-history-dot" :style="`background: ${activityDotColor[entry.type] || '#8a8a86'}`" />
|
|
<div v-if="i < filteredHistory.length - 1" class="cp-history-line" />
|
|
</div>
|
|
<div class="cp-history-content">
|
|
<div class="cp-history-top">
|
|
<span class="cp-history-action">{{ entry.action }}</span>
|
|
<span class="cp-history-type-badge" :style="`background: ${activityDotColor[entry.type] || '#8a8a86'}18; color: ${activityDotColor[entry.type] || '#8a8a86'}`">
|
|
{{ entry.type }}
|
|
</span>
|
|
</div>
|
|
<p v-if="entry.detail" class="cp-history-detail">{{ entry.detail }}</p>
|
|
<div class="cp-history-meta">
|
|
<UIcon name="i-heroicons-user-circle" style="width: 12px; height: 12px; opacity: 0.5;" />
|
|
<span>{{ entry.actor }}</span>
|
|
<span class="cp-sep" />
|
|
<span>{{ entry.date }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="cp-empty-msg" style="padding: 20px;">No entries match this filter</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── RELATIONSHIPS TAB ── -->
|
|
<div v-if="activeTab === 'relationships'" class="cp-tab-content">
|
|
<!-- Household Members -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head cp-card-head-compact">
|
|
<span>Household Members</span>
|
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-plus">Add</UButton>
|
|
</div>
|
|
<div class="cp-rel-empty">
|
|
<UIcon name="i-heroicons-users" class="cp-rel-empty-icon" />
|
|
<p>No linked household members</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Business Affiliations -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head cp-card-head-compact">
|
|
<span>Business Affiliations</span>
|
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-plus">Add</UButton>
|
|
</div>
|
|
<template v-if="hasRelationship">
|
|
<div class="cp-row">
|
|
<UIcon name="i-heroicons-building-office-2" class="cp-rel-icon" />
|
|
<div class="cp-row-main">
|
|
<p class="cp-row-title">Corporación Tecnológica del Valle</p>
|
|
<p class="cp-row-sub">Employee · Linked to cust-009</p>
|
|
</div>
|
|
<NuxtLink to="/customers/cust-009">
|
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-top-right-on-square">View</UButton>
|
|
</NuxtLink>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="cp-rel-empty">
|
|
<UIcon name="i-heroicons-building-office-2" class="cp-rel-empty-icon" />
|
|
<p>No business affiliations</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Professional Contacts -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head cp-card-head-compact">
|
|
<span>Professional Contacts</span>
|
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-plus">Add</UButton>
|
|
</div>
|
|
<div class="cp-rel-empty">
|
|
<UIcon name="i-heroicons-briefcase" class="cp-rel-empty-icon" />
|
|
<p>No professional contacts</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── NOTES TAB ── -->
|
|
<div v-if="activeTab === 'notes'" class="cp-tab-content">
|
|
<!-- New note -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-pencil-square" style="width: 14px; height: 14px; color: #01696f;" />
|
|
<span>New Note</span>
|
|
</div>
|
|
<div class="cp-note-input">
|
|
<textarea v-model="newNote" class="cp-textarea" placeholder="Add a note about this customer..." rows="3" />
|
|
<div class="cp-note-actions">
|
|
<UButton size="xs" color="primary" :disabled="!newNote.trim()">Save Note</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Existing notes -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-bookmark" style="width: 14px; height: 14px; color: #01696f;" />
|
|
<span>Pinned Notes</span>
|
|
</div>
|
|
<div v-for="(note, i) in mockNotes" :key="note.id" class="cp-note-item" :class="i < mockNotes.length - 1 ? 'cp-row-border' : ''">
|
|
<div class="cp-note-header">
|
|
<span class="cp-note-author">{{ note.author }}</span>
|
|
<span class="cp-note-date">{{ note.date }}</span>
|
|
<UIcon v-if="note.pinned" name="i-heroicons-bookmark-solid" style="width: 12px; height: 12px; color: #d4a017;" />
|
|
</div>
|
|
<p class="cp-note-text">{{ note.text }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ DOCUMENTS ═══ -->
|
|
<div class="cp-card">
|
|
<div class="cp-card-head">
|
|
<UIcon name="i-heroicons-folder-open" style="width: 16px; height: 16px; color: #01696f;" />
|
|
<span>Documents</span>
|
|
</div>
|
|
<div v-for="(cat, i) in documentCategories" :key="cat.name" class="cp-doc-row" :class="i < documentCategories.length - 1 ? 'cp-row-border' : ''">
|
|
<UIcon :name="cat.icon" class="cp-doc-icon" />
|
|
<span class="cp-doc-name">{{ cat.name }}</span>
|
|
<span class="cp-doc-count">{{ cat.count }}</span>
|
|
<UButton size="xs" color="neutral" variant="ghost" :disabled="cat.count === 0">View</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ═══ Page layout ═══ */
|
|
.cp-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
/* ═══ Header ═══ */
|
|
.cp-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
.cp-header-info {
|
|
min-width: 0;
|
|
}
|
|
.cp-avatar {
|
|
width: 52px;
|
|
height: 52px;
|
|
border-radius: 12px;
|
|
background: rgba(1, 105, 111, 0.08);
|
|
color: #01696f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-header-name-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-name {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-type-badge {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 2px 8px;
|
|
border-radius: 6px;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
}
|
|
.cp-tier-badge {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 2px 8px;
|
|
border-radius: 9999px;
|
|
}
|
|
.cp-tier-customer {
|
|
background: rgba(15, 123, 95, 0.08);
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-tier-lead {
|
|
background: rgba(1, 105, 111, 0.08);
|
|
color: #01696f;
|
|
}
|
|
.cp-tier-quick-lead {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: #8a8a86;
|
|
}
|
|
.cp-tier-cancelled {
|
|
background: rgba(193, 56, 56, 0.08);
|
|
color: #c13838;
|
|
}
|
|
.cp-tag {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-header-meta {
|
|
margin-top: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-sep {
|
|
width: 3px;
|
|
height: 3px;
|
|
border-radius: 50%;
|
|
background: rgba(0, 0, 0, 0.15);
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
}
|
|
|
|
/* ═══ Card ═══ */
|
|
.cp-card {
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cp-card-hover {
|
|
transition: all 150ms ease;
|
|
cursor: pointer;
|
|
}
|
|
.cp-card-hover:hover {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
border-color: rgba(1, 105, 111, 0.15);
|
|
}
|
|
.cp-card-historical {
|
|
opacity: 0.65;
|
|
}
|
|
.cp-card-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-card-head-compact {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* ═══ Account Orientation ═══ */
|
|
.cp-orient-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-dot-green {
|
|
background: #0f7b5f;
|
|
}
|
|
.cp-dot-gray {
|
|
background: #8a8a86;
|
|
}
|
|
.cp-orient-good {
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-orient-warn {
|
|
color: #c27b1a;
|
|
}
|
|
.cp-orient-bad {
|
|
color: #c13838;
|
|
}
|
|
.cp-orient-muted {
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-orient-tag {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
}
|
|
.cp-gap-chip {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(193, 56, 56, 0.06);
|
|
color: #964219;
|
|
}
|
|
/* ═══ KPI Strip ═══ */
|
|
.cp-kpi-strip {
|
|
display: flex;
|
|
align-items: stretch;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cp-kpi {
|
|
flex: 1;
|
|
padding: 16px 20px;
|
|
min-width: 0;
|
|
}
|
|
.cp-kpi-div {
|
|
width: 1px;
|
|
background: rgba(0, 0, 0, 0.06);
|
|
margin: 12px 0;
|
|
}
|
|
.cp-kpi-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
}
|
|
.cp-kpi-value {
|
|
margin-top: 4px;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.cp-kpi-good {
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-kpi-bad {
|
|
color: #c13838;
|
|
}
|
|
.cp-kpi-warn {
|
|
color: #c27b1a;
|
|
}
|
|
.cp-kpi-sub {
|
|
margin-top: 2px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
@media (max-width: 1023px) {
|
|
.cp-kpi-strip {
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-kpi {
|
|
min-width: 45%;
|
|
}
|
|
.cp-kpi-div {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ═══ Service Actions ═══ */
|
|
.cp-service-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0;
|
|
}
|
|
.cp-service-section {
|
|
padding: 16px 20px;
|
|
border-right: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.cp-service-section:last-child {
|
|
border-right: none;
|
|
}
|
|
.cp-service-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
margin-bottom: 12px;
|
|
}
|
|
.cp-timeline {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.cp-timeline-item {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
}
|
|
.cp-timeline-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
margin-top: 5px;
|
|
}
|
|
.cp-timeline-content {
|
|
min-width: 0;
|
|
}
|
|
.cp-timeline-text {
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
line-height: 1.4;
|
|
}
|
|
.cp-timeline-date {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 1px;
|
|
}
|
|
.cp-action-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.cp-action-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.cp-action-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
color: #8a8a86;
|
|
}
|
|
.cp-icon-green {
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-action-text {
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1;
|
|
}
|
|
.cp-action-date {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-empty-msg {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
padding: 8px 0;
|
|
}
|
|
@media (max-width: 767px) {
|
|
.cp-service-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.cp-service-section {
|
|
border-right: none;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.cp-service-section:last-child {
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
|
|
/* ═══ Main Grid ═══ */
|
|
.cp-main-grid {
|
|
display: grid;
|
|
gap: 20px;
|
|
grid-template-columns: 1fr;
|
|
}
|
|
@media (min-width: 1024px) {
|
|
.cp-main-grid {
|
|
grid-template-columns: 1fr 2fr;
|
|
}
|
|
}
|
|
.cp-details-col {
|
|
align-self: start;
|
|
}
|
|
.cp-tabbed-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* ═══ Details ═══ */
|
|
.cp-detail-list {
|
|
padding: 4px 0;
|
|
}
|
|
.cp-detail-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
padding: 8px 20px;
|
|
}
|
|
.cp-detail-row:hover {
|
|
background: rgba(0, 0, 0, 0.015);
|
|
}
|
|
.cp-detail-label {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-detail-value {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
text-align: right;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.cp-mono {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ═══ Tabs ═══ */
|
|
.cp-tabs {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cp-tab-on {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.cp-tab-off {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-tab-off:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-tab-count {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 5px;
|
|
border-radius: 9999px;
|
|
background: rgba(0, 0, 0, 0.06);
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-tab-count-on {
|
|
background: rgba(1, 105, 111, 0.1);
|
|
color: #01696f;
|
|
}
|
|
.cp-tab-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ═══ Coverage bar ═══ */
|
|
.cp-coverage-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 10px 16px;
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.02);
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-coverage-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
}
|
|
.cp-coverage-dot-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.cp-coverage-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
.cp-coverage-active {
|
|
background: #01696f;
|
|
}
|
|
.cp-coverage-inactive {
|
|
background: rgba(0, 0, 0, 0.1);
|
|
}
|
|
.cp-coverage-line-label {
|
|
font-size: 11px;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-coverage-line-missing {
|
|
color: var(--text-muted);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
/* ═══ Coverage gap CTA ═══ */
|
|
.cp-gap-cta {
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
background: rgba(217,119,6,0.04);
|
|
border: 1px solid rgba(217,119,6,0.12);
|
|
}
|
|
.cp-gap-quote-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 3px 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: #01696f;
|
|
background: rgba(1,105,111,0.06);
|
|
border-radius: 6px;
|
|
text-decoration: none;
|
|
transition: background 150ms ease;
|
|
}
|
|
.cp-gap-quote-btn:hover {
|
|
background: rgba(1,105,111,0.12);
|
|
}
|
|
|
|
/* ═══ Policy cards ═══ */
|
|
.cp-policy-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.cp-group-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
padding: 4px 0;
|
|
}
|
|
.cp-policy-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
}
|
|
.cp-pol-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-pol-icon-muted {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: #8a8a86;
|
|
}
|
|
.cp-pol-info {
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.cp-pol-name-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-pol-product {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-line-badge {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
}
|
|
.cp-status-active {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
background: rgba(15, 123, 95, 0.08);
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-status-pending-badge {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
background: rgba(194, 123, 26, 0.08);
|
|
color: #964219;
|
|
}
|
|
.cp-status-cancelled {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
background: rgba(193, 56, 56, 0.06);
|
|
color: #c13838;
|
|
}
|
|
.cp-pol-meta {
|
|
margin-top: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-pol-details {
|
|
margin-top: 4px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-pol-premium {
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-pol-amount {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-pol-period {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-historical-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px 0;
|
|
}
|
|
.cp-historical-toggle:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ═══ Rows / Claims / Activity ═══ */
|
|
.cp-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 20px;
|
|
}
|
|
.cp-row-border {
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
.cp-row:hover {
|
|
background: rgba(0, 0, 0, 0.015);
|
|
}
|
|
.cp-row-main {
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.cp-row-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.cp-row-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-row-sub {
|
|
margin-top: 2px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-row-text {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-row-date {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-row-amount {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-primary);
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-status {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
white-space: nowrap;
|
|
}
|
|
.cp-status-ok {
|
|
background: rgba(15, 123, 95, 0.08);
|
|
color: #0f7b5f;
|
|
}
|
|
.cp-status-warn {
|
|
background: rgba(194, 123, 26, 0.08);
|
|
color: #964219;
|
|
}
|
|
.cp-status-bad {
|
|
background: rgba(193, 56, 56, 0.08);
|
|
color: #c13838;
|
|
}
|
|
.cp-status-pending {
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: #8a8a86;
|
|
}
|
|
.cp-activity-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-activity-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ═══ Table ═══ */
|
|
.cp-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
.cp-table th {
|
|
text-align: left;
|
|
padding: 10px 16px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.cp-table td {
|
|
padding: 10px 16px;
|
|
color: var(--text-primary);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
.cp-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.cp-table tr:hover td {
|
|
background: rgba(0, 0, 0, 0.015);
|
|
}
|
|
|
|
/* ═══ Relationships ═══ */
|
|
.cp-rel-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 28px 20px;
|
|
color: var(--text-muted);
|
|
font-size: 12px;
|
|
}
|
|
.cp-rel-empty-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
color: rgba(0, 0, 0, 0.12);
|
|
}
|
|
.cp-rel-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
color: #01696f;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ═══ Notes ═══ */
|
|
.cp-note-input {
|
|
padding: 16px 20px;
|
|
}
|
|
.cp-textarea {
|
|
width: 100%;
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
background: #ffffff;
|
|
resize: vertical;
|
|
outline: none;
|
|
font-family: inherit;
|
|
transition: border-color 150ms ease;
|
|
}
|
|
.cp-textarea:focus {
|
|
border-color: #01696f;
|
|
}
|
|
.cp-textarea::placeholder {
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-note-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 10px;
|
|
}
|
|
.cp-note-item {
|
|
padding: 14px 20px;
|
|
}
|
|
.cp-note-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.cp-note-author {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-note-date {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-note-text {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* ═══ Documents ═══ */
|
|
.cp-doc-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 20px;
|
|
}
|
|
.cp-doc-row:hover {
|
|
background: rgba(0, 0, 0, 0.015);
|
|
}
|
|
.cp-doc-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
color: #01696f;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-doc-name {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
flex: 1;
|
|
}
|
|
.cp-doc-count {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ═══ Utility ═══ */
|
|
.cp-text-right {
|
|
text-align: right;
|
|
}
|
|
.cp-text-muted {
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-text-xs {
|
|
font-size: 11px;
|
|
}
|
|
.cp-text-medium {
|
|
font-weight: 500;
|
|
}
|
|
.cp-tabular {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* ═══ Sales Agent Assignment ═══ */
|
|
.cp-detail-row-agent {
|
|
align-items: center;
|
|
padding: 6px 20px;
|
|
}
|
|
.cp-agent-assign {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.cp-agent-select {
|
|
width: 220px;
|
|
font-size: 12px;
|
|
}
|
|
.cp-agent-link {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 6px;
|
|
color: #01696f;
|
|
transition: background 150ms ease;
|
|
}
|
|
.cp-agent-link:hover {
|
|
background: rgba(1, 105, 111, 0.06);
|
|
}
|
|
|
|
/* ═══ History / Audit Log ═══ */
|
|
.cp-history-filters {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
margin-bottom: 8px;
|
|
}
|
|
.cp-history-filter {
|
|
padding: 5px 12px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cp-history-filter-on {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.cp-history-filter-off {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-history-filter-off:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-history-count {
|
|
margin-left: auto;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-history-row {
|
|
display: flex;
|
|
gap: 0;
|
|
padding: 0;
|
|
}
|
|
.cp-history-dot-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
width: 40px;
|
|
flex-shrink: 0;
|
|
padding-top: 18px;
|
|
}
|
|
.cp-history-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
z-index: 1;
|
|
}
|
|
.cp-history-line {
|
|
width: 2px;
|
|
flex: 1;
|
|
background: rgba(0, 0, 0, 0.06);
|
|
margin-top: 4px;
|
|
}
|
|
.cp-history-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 14px 16px 14px 0;
|
|
}
|
|
.cp-history-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-history-action {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-history-type-badge {
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
.cp-history-detail {
|
|
margin-top: 3px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-history-meta {
|
|
margin-top: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* ═══ Account Orientation (warm redesign) ═══ */
|
|
.cp-orient-card {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cp-orient-summary {
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.cp-grade-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 16px;
|
|
border-radius: 10px;
|
|
border: 1.5px solid;
|
|
align-self: flex-start;
|
|
}
|
|
.cp-grade-name {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
}
|
|
.cp-grade-score {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 1px;
|
|
}
|
|
.cp-orient-facts {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.cp-orient-fact {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
.cp-orient-fact-label {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.cp-orient-fact-value {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-orient-events {
|
|
padding: 20px;
|
|
border-left: 1px solid rgba(0, 0, 0, 0.06);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.cp-orient-events-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.cp-events-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.cp-event-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
}
|
|
.cp-event-clickable {
|
|
cursor: pointer;
|
|
transition: background 150ms ease;
|
|
}
|
|
.cp-event-clickable:hover {
|
|
background: rgba(1, 105, 111, 0.06);
|
|
}
|
|
.cp-event-upcoming {
|
|
background: rgba(0, 0, 0, 0.02);
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-event-urgent {
|
|
background: rgba(193, 56, 56, 0.05);
|
|
color: #964219;
|
|
}
|
|
.cp-event-action {
|
|
background: rgba(194, 123, 26, 0.05);
|
|
color: #964219;
|
|
}
|
|
.cp-event-label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.cp-event-date {
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
}
|
|
.cp-orient-quiet {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
}
|
|
@media (max-width: 767px) {
|
|
.cp-orient-card {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.cp-orient-events {
|
|
border-left: none;
|
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
}
|
|
|
|
/* ═══ Quick Policy Access ═══ */
|
|
.cp-quick-policies {
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
|
overflow: hidden;
|
|
}
|
|
.cp-quick-pol-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
flex-wrap: wrap;
|
|
}
|
|
.cp-quick-pol-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-quick-pol-count {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
padding: 1px 8px;
|
|
border-radius: 9999px;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
}
|
|
.cp-quick-pol-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.cp-quick-pol-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 0;
|
|
}
|
|
.cp-quick-pol-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
|
transition: background 150ms ease;
|
|
cursor: pointer;
|
|
}
|
|
.cp-quick-pol-chip:hover {
|
|
background: rgba(0, 0, 0, 0.015);
|
|
}
|
|
.cp-quick-pol-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
color: #01696f;
|
|
}
|
|
.cp-quick-pol-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.cp-quick-pol-name {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.cp-quick-pol-id {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
font-family: monospace;
|
|
}
|
|
.cp-quick-pol-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
.cp-quick-pol-premium {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.cp-quick-pol-dl {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: rgba(1, 105, 111, 0.06);
|
|
color: #01696f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cp-quick-pol-dl:hover {
|
|
background: rgba(1, 105, 111, 0.12);
|
|
}
|
|
</style>
|