Files
policy-ui/app/pages/policies/[id].vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

1770 lines
64 KiB
Vue

<script setup lang="ts">
definePageMeta({ ssr: false })
usePageTitle('Policy Detail')
const route = useRoute()
const policyId = route.params.id as string
// ── Types ────────────────────────────────────────────────────────────────────
interface PolicyDetail {
id: string
policyNumber: string
status: 'active' | 'pending' | 'lapsed' | 'cancelled' | 'expired'
lob: 'Auto' | 'Health' | 'Life' | 'General Risk'
insurer: string
insurerLogo: string
effectiveDate: string
expirationDate: string
issueDate: string
lastRenewalDate: string | null
annualPremium: number
paymentFrequency: 'Monthly' | 'Quarterly' | 'Semi-annual' | 'Annual'
nextPaymentDate: string
paymentStatus: 'current' | 'overdue' | 'grace_period'
coverageAmount: number
deductible: number
coverageDetails: { label: string; value: string }[]
titular: { name: string; cedula: string; phone: string; email: string }
agent: string
vehicle?: { make: string; model: string; year: number; vin: string; plate: string; color: string; isFleet: boolean; fleetName?: string; groupDiscount?: string }
dependents?: { name: string; relationship: string; dob: string; cedula: string }[]
beneficiaries?: { name: string; relationship: string; percentage: number; type: 'primary' | 'contingent' }[]
mortgageHolder?: { company: string; loanNumber: string; type: string }
property?: { address: string; type: string; constructionYear: number; area: string; value: number }
endorsements: { id: string; date: string; type: string; description: string; premium: number }[]
claims: { id: string; date: string; type: string; status: string; amount: number; description: string }[]
renewals: { period: string; premium: number; status: string }[]
documents: { id: string; name: string; type: string; date: string; size: string }[]
activity: { date: string; agent: string; action: string; detail: string }[]
economicGroup: string | null
relatedPolicies: { id: string; policyNumber: string; lob: string; titular: string; status: string }[]
billing: {
outstandingBalance: number
lastPaymentDate: string
lastPaymentAmount: number
paymentMethod: string
paymentHistory: { date: string; amount: number; method: string; status: string }[]
}
commission: {
rate: number
amount: number
status: string
producer: string
earned: number
paid: number
owed: number
nextPaymentDate: string
}
}
// ── Mock data ────────────────────────────────────────────────────────────────
const policy = ref<PolicyDetail>({
id: 'POL-4401',
policyNumber: 'AUTO-2024-004401',
status: 'active',
lob: 'Auto',
insurer: 'ASSA Compañía de Seguros',
insurerLogo: 'i-heroicons-shield-check',
effectiveDate: '2024-03-15',
expirationDate: '2025-03-15',
issueDate: '2024-03-10',
lastRenewalDate: '2024-03-15',
annualPremium: 1_245_000,
paymentFrequency: 'Quarterly',
nextPaymentDate: '2025-06-15',
paymentStatus: 'current',
coverageAmount: 45_000_000,
deductible: 500_000,
coverageDetails: [
{ label: 'Responsabilidad Civil', value: '₡25,000,000' },
{ label: 'Daños Propios', value: '₡45,000,000' },
{ label: 'Robo Total', value: '₡45,000,000' },
{ label: 'Asistencia en Carretera', value: 'Incluida' },
{ label: 'Vidrios y Espejos', value: '₡1,500,000' },
{ label: 'Gastos Médicos', value: '₡5,000,000' },
{ label: 'Extensión Territorial', value: 'Centroamérica' },
],
titular: {
name: 'Transportes del Norte S.A.',
cedula: '3-101-456789',
phone: '+506 2222-8800',
email: 'seguros@transportesnorte.cr',
},
agent: 'María Fernanda Solís',
vehicle: {
make: 'Toyota',
model: 'Hilux SRV 4x4',
year: 2024,
vin: 'JT3HN87R5Y0234567',
plate: '123-ABC',
color: 'Blanco Perlado',
isFleet: true,
fleetName: 'Flota Transportes Norte',
groupDiscount: '12%',
},
endorsements: [
{ id: 'END-001', date: '2024-06-20', type: 'Adición', description: 'Agregar cobertura de vidrios polarizados', premium: 35_000 },
{ id: 'END-002', date: '2024-09-05', type: 'Modificación', description: 'Cambio de deducible de ₡750,000 a ₡500,000', premium: 48_000 },
],
claims: [
{ id: 'CLM-2201', date: '2024-05-12', type: 'Colisión', status: 'Resuelto', amount: 2_350_000, description: 'Colisión lateral en Ruta 1, San José. Daño en puerta derecha y guardafango.' },
{ id: 'CLM-2202', date: '2024-08-23', type: 'Vidrios', status: 'Resuelto', amount: 185_000, description: 'Reemplazo de parabrisas por impacto de piedra en carretera Interamericana.' },
{ id: 'CLM-2203', date: '2025-01-10', type: 'Robo parcial', status: 'En revisión', amount: 890_000, description: 'Robo de accesorios del vehículo estacionado en parqueo de Alajuela.' },
],
renewals: [
{ period: '2021-2022', premium: 980_000, status: 'Completado' },
{ period: '2022-2023', premium: 1_050_000, status: 'Completado' },
{ period: '2023-2024', premium: 1_150_000, status: 'Completado' },
{ period: '2024-2025', premium: 1_245_000, status: 'Vigente' },
],
documents: [
{ id: 'DOC-01', name: 'Póliza Original AUTO-2024-004401', type: 'Póliza', date: '2024-03-10', size: '2.4 MB' },
{ id: 'DOC-02', name: 'Endoso END-001 — Vidrios Polarizados', type: 'Endoso', date: '2024-06-20', size: '580 KB' },
{ id: 'DOC-03', name: 'Endoso END-002 — Cambio Deducible', type: 'Endoso', date: '2024-09-05', size: '612 KB' },
{ id: 'DOC-04', name: 'Certificado de Seguro Vigente', type: 'Certificado', date: '2025-01-15', size: '340 KB' },
{ id: 'DOC-05', name: 'Recibo de Prima Q1-2025', type: 'Recibo', date: '2025-03-15', size: '128 KB' },
],
activity: [
{ date: '2025-03-15', agent: 'María Fernanda Solís', action: 'Renovación procesada', detail: 'Renovación período 2024-2025 confirmada con ASSA. Prima ajustada +8.3%.' },
{ date: '2025-01-15', agent: 'Sistema', action: 'Certificado emitido', detail: 'Certificado de seguro vigente generado automáticamente.' },
{ date: '2025-01-10', agent: 'Carlos Méndez', action: 'Reclamo abierto', detail: 'CLM-2203 registrado: robo parcial de accesorios en Alajuela.' },
{ date: '2024-09-05', agent: 'María Fernanda Solís', action: 'Endoso aplicado', detail: 'END-002: Reducción de deducible aprobada por ASSA.' },
{ date: '2024-06-20', agent: 'María Fernanda Solís', action: 'Endoso aplicado', detail: 'END-001: Cobertura de vidrios polarizados agregada.' },
{ date: '2024-03-10', agent: 'María Fernanda Solís', action: 'Póliza emitida', detail: 'Póliza AUTO-2024-004401 emitida por ASSA para Toyota Hilux 2024.' },
],
billing: {
outstandingBalance: 0,
lastPaymentDate: '2025-03-15',
lastPaymentAmount: 311_250,
paymentMethod: 'Transferencia bancaria',
paymentHistory: [
{ date: '2025-03-15', amount: 311_250, method: 'Transferencia', status: 'Pagado' },
{ date: '2024-12-15', amount: 311_250, method: 'Transferencia', status: 'Pagado' },
{ date: '2024-09-15', amount: 311_250, method: 'Transferencia', status: 'Pagado' },
{ date: '2024-06-15', amount: 311_250, method: 'Cheque', status: 'Pagado' },
],
},
commission: {
rate: 15,
amount: 186_750,
status: 'Earned',
producer: 'María Fernanda Solís',
earned: 186_750,
paid: 140_063,
owed: 46_687,
nextPaymentDate: '2025-04-30',
},
economicGroup: 'Grupo Transportes del Norte',
relatedPolicies: [
{ id: 'POL-4402', policyNumber: 'AUTO-2024-004402', lob: 'Auto', titular: 'Transportes del Norte S.A.', status: 'active' },
{ id: 'POL-3310', policyNumber: 'PROP-2023-003310', lob: 'General Risk', titular: 'Transportes del Norte S.A.', status: 'active' },
],
})
// ── Tabs ──────────────────────────────────────────────────────────────────────
type TabId = 'overview' | 'claims' | 'renewals' | 'documents' | 'view-policy' | 'activity'
const activeTab = ref<TabId>('overview')
const tabs: { id: TabId; label: string; icon: string }[] = [
{ id: 'overview', label: 'Overview', icon: 'i-heroicons-squares-2x2' },
{ id: 'claims', label: 'Claims', icon: 'i-heroicons-document-text' },
{ id: 'renewals', label: 'Renewals', icon: 'i-heroicons-arrow-path' },
{ id: 'documents', label: 'Documents', icon: 'i-heroicons-folder-open' },
{ id: 'view-policy', label: 'View Policy', icon: 'i-heroicons-document-magnifying-glass' },
{ id: 'activity', label: 'Activity', icon: 'i-heroicons-clock' },
]
// ── Helpers ───────────────────────────────────────────────────────────────────
const fmtMoney = (n: number) => '₡' + n.toLocaleString('es-CR')
const fmtDate = (d: string) => new Date(d).toLocaleDateString('es-CR', { day: '2-digit', month: 'short', year: 'numeric' })
const statusClass = (s: string) => {
const map: Record<string, string> = {
active: 'pol-status-active', pending: 'pol-status-pending', lapsed: 'pol-status-lapsed',
cancelled: 'pol-status-cancelled', expired: 'pol-status-expired',
'Resuelto': 'pol-status-active', 'En revisión': 'pol-status-pending', 'Denegado': 'pol-status-lapsed',
'Completado': 'pol-status-active', 'Vigente': 'pol-status-pending',
}
return map[s] ?? 'pol-status-cancelled'
}
const statusLabel = (s: string) => {
const map: Record<string, string> = { active: 'Active', pending: 'Pending', lapsed: 'Lapsed', cancelled: 'Cancelled', expired: 'Expired' }
return map[s] ?? s
}
const paymentStatusClass = (s: string) => {
const map: Record<string, string> = { current: 'pol-pay-current', overdue: 'pol-pay-overdue', grace_period: 'pol-pay-grace' }
return map[s] ?? ''
}
const paymentStatusLabel = (s: string) => {
const map: Record<string, string> = { current: 'Current', overdue: 'Overdue', grace_period: 'Grace Period' }
return map[s] ?? s
}
const docTypeClass = (t: string) => {
const map: Record<string, string> = {
'Póliza': 'pol-doc-poliza', 'Endoso': 'pol-doc-endoso',
'Certificado': 'pol-doc-certificado', 'Recibo': 'pol-doc-recibo',
}
return map[t] ?? 'pol-doc-other'
}
const renewalChange = (idx: number) => {
if (idx === 0) return null
const prev = policy.value.renewals[idx - 1].premium
const curr = policy.value.renewals[idx].premium
return ((curr - prev) / prev * 100).toFixed(1)
}
const lobIcon = (lob: string) => {
const map: Record<string, string> = { Auto: 'i-heroicons-truck', Health: 'i-heroicons-heart', Life: 'i-heroicons-shield-check', 'General Risk': 'i-heroicons-building-office-2' }
return map[lob] ?? 'i-heroicons-document'
}
const daysUntilExpiration = computed(() => {
const now = new Date()
const exp = new Date(policy.value.expirationDate)
return Math.max(0, Math.ceil((exp.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)))
})
const expandedClaim = ref<string | null>(null)
const toggleClaim = (id: string) => { expandedClaim.value = expandedClaim.value === id ? null : id }
// ── Viewable Documents (View Policy tab) ────────────────────────────────────
const viewableDocs = [
{ id: 'VD-001', name: 'Póliza Original AUTO-2024-004401', type: 'policy', url: '#', pages: 12 },
{ id: 'VD-002', name: 'Endoso END-001 — Vidrios Polarizados', type: 'endorsement', url: '#', pages: 2 },
{ id: 'VD-003', name: 'Endoso END-002 — Reducción Deducible', type: 'endorsement', url: '#', pages: 1 },
{ id: 'VD-004', name: 'Certificado de Seguro Vigente', type: 'certificate', url: '#', pages: 1 },
{ id: 'VD-005', name: 'Póliza Relacionada AUTO-2024-004402', type: 'related', url: '#', pages: 8 },
{ id: 'VD-006', name: 'Póliza Relacionada PROP-2023-003310', type: 'related', url: '#', pages: 15 },
]
const selectedDoc = ref(viewableDocs[0])
const viewableDocGroups = computed(() => [
{ label: 'Policy', items: viewableDocs.filter(d => d.type === 'policy') },
{ label: 'Endorsements', items: viewableDocs.filter(d => d.type === 'endorsement') },
{ label: 'Certificates', items: viewableDocs.filter(d => d.type === 'certificate') },
{ label: 'Related Policies', items: viewableDocs.filter(d => d.type === 'related') },
])
const docTypeBadgeClass = (t: string) => {
const map: Record<string, string> = {
policy: 'pol-vd-badge-policy', endorsement: 'pol-vd-badge-endorsement',
certificate: 'pol-vd-badge-certificate', related: 'pol-vd-badge-related',
}
return map[t] ?? ''
}
const docTypeBadgeLabel = (t: string) => {
const map: Record<string, string> = {
policy: 'Policy', endorsement: 'Endorsement',
certificate: 'Certificate', related: 'Related',
}
return map[t] ?? t
}
</script>
<template>
<div class="pol-root mx-auto max-w-6xl pb-12">
<!-- Back link -->
<NuxtLink to="/policies" class="pol-back-link">
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
Back to Policies
</NuxtLink>
<!-- Header -->
<div class="pol-detail-header">
<div class="pol-detail-header-left">
<div class="pol-detail-title-row">
<h1 class="pol-detail-title">{{ policy.policyNumber }}</h1>
<span class="pol-status-badge pol-status-pill" :class="statusClass(policy.status)">{{ statusLabel(policy.status) }}</span>
<span class="pol-lob-chip" :class="`pol-lob-${policy.lob.toLowerCase().replace(' ', '-')}`">
<UIcon :name="lobIcon(policy.lob)" class="w-3 h-3" />
{{ policy.lob }}
</span>
</div>
<div class="pol-detail-subtitle">
<UIcon :name="policy.insurerLogo" class="w-4 h-4" style="color: #01696f;" />
<span>{{ policy.insurer }}</span>
<span class="pol-detail-sep">·</span>
<span>{{ policy.titular.name }}</span>
</div>
</div>
<div class="pol-detail-header-right">
<button class="pol-btn-secondary">
<UIcon name="i-heroicons-arrow-path" class="w-3.5 h-3.5" />
Renew
</button>
<button class="pol-btn-secondary">
<UIcon name="i-heroicons-arrow-down-tray" class="w-3.5 h-3.5" />
Download
</button>
<button class="pol-btn-secondary">
<UIcon name="i-heroicons-printer" class="w-3.5 h-3.5" />
Print
</button>
</div>
</div>
<!-- Tab navigation -->
<div class="pol-main-tabs">
<button
v-for="t in tabs"
:key="t.id"
type="button"
class="pol-main-tab"
:class="activeTab === t.id ? 'pol-main-tab-on' : 'pol-main-tab-off'"
@click="activeTab = t.id"
>
<UIcon :name="t.icon" class="w-3.5 h-3.5" />
{{ t.label }}
<span v-if="t.id === 'claims'" class="pol-tab-count" :class="activeTab === 'claims' ? 'pol-tab-count-on' : ''">{{ policy.claims.length }}</span>
<span v-if="t.id === 'documents'" class="pol-tab-count" :class="activeTab === 'documents' ? 'pol-tab-count-on' : ''">{{ policy.documents.length }}</span>
</button>
</div>
<!-- Quick Info Strip -->
<div class="pol-quick-strip">
<div class="pol-quick-item">
<UIcon name="i-heroicons-calendar" class="w-4 h-4 pol-quick-icon" />
<div class="pol-quick-data">
<span class="pol-quick-value" :class="daysUntilExpiration <= 30 ? 'pol-quick-warn' : ''">{{ daysUntilExpiration }}</span>
<span class="pol-quick-label">Days to Expiry</span>
</div>
</div>
<div class="pol-quick-sep" />
<div class="pol-quick-item">
<UIcon name="i-heroicons-banknotes" class="w-4 h-4 pol-quick-icon" />
<div class="pol-quick-data">
<span class="pol-quick-value">{{ fmtMoney(policy.billing.outstandingBalance) }}</span>
<span class="pol-quick-label">Outstanding</span>
</div>
</div>
<div class="pol-quick-sep" />
<div class="pol-quick-item">
<UIcon name="i-heroicons-document-text" class="w-4 h-4 pol-quick-icon" />
<div class="pol-quick-data">
<span class="pol-quick-value">{{ policy.claims.length }}</span>
<span class="pol-quick-label">Total Claims</span>
</div>
</div>
<div class="pol-quick-sep" />
<div class="pol-quick-item">
<UIcon name="i-heroicons-currency-dollar" class="w-4 h-4 pol-quick-icon" />
<div class="pol-quick-data">
<span class="pol-quick-value">{{ fmtMoney(policy.commission.amount) }}</span>
<span class="pol-quick-label">Commission</span>
</div>
</div>
</div>
<!-- OVERVIEW TAB -->
<template v-if="activeTab === 'overview'">
<div class="pol-grid-2">
<!-- Policy Information -->
<div class="pol-card pol-card-padded pol-grid-span-2">
<div class="pol-card-title">Policy Information</div>
<div class="pol-info-grid">
<div class="pol-info-item">
<span class="pol-label">Policy Number</span>
<span class="pol-value pol-mono">{{ policy.policyNumber }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Status</span>
<span class="pol-status-badge pol-status-pill" :class="statusClass(policy.status)">{{ statusLabel(policy.status) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Line of Business</span>
<span class="pol-value">{{ policy.lob }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Insurer</span>
<span class="pol-value">{{ policy.insurer }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Effective Date</span>
<span class="pol-value">{{ fmtDate(policy.effectiveDate) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Expiration Date</span>
<span class="pol-value">{{ fmtDate(policy.expirationDate) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Issue Date</span>
<span class="pol-value">{{ fmtDate(policy.issueDate) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Last Renewal</span>
<span class="pol-value">{{ policy.lastRenewalDate ? fmtDate(policy.lastRenewalDate) : '—' }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Annual Premium</span>
<span class="pol-value pol-value-bold">{{ fmtMoney(policy.annualPremium) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Payment Frequency</span>
<span class="pol-value">{{ policy.paymentFrequency }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Next Payment</span>
<span class="pol-value">{{ fmtDate(policy.nextPaymentDate) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Payment Status</span>
<span class="pol-pay-badge" :class="paymentStatusClass(policy.paymentStatus)">{{ paymentStatusLabel(policy.paymentStatus) }}</span>
</div>
</div>
</div>
<!-- Coverage Details -->
<div class="pol-card pol-card-padded">
<div class="pol-card-title">Coverage Details</div>
<div class="pol-coverage-header">
<div class="pol-info-item">
<span class="pol-label">Coverage Amount</span>
<span class="pol-value pol-value-bold">{{ fmtMoney(policy.coverageAmount) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Deductible</span>
<span class="pol-value pol-value-bold">{{ fmtMoney(policy.deductible) }}</span>
</div>
</div>
<div class="pol-divider" />
<div class="pol-coverage-list">
<div v-for="c in policy.coverageDetails" :key="c.label" class="pol-coverage-row">
<span class="pol-coverage-label">{{ c.label }}</span>
<span class="pol-coverage-value">{{ c.value }}</span>
</div>
</div>
</div>
<!-- Policyholder -->
<div class="pol-card pol-card-padded">
<div class="pol-card-title">Policyholder</div>
<div class="pol-holder-info">
<div class="pol-holder-avatar">
<UIcon name="i-heroicons-building-office" class="w-5 h-5" />
</div>
<div>
<div class="pol-holder-name">{{ policy.titular.name }}</div>
<div class="pol-holder-cedula">{{ policy.titular.cedula }}</div>
</div>
</div>
<div class="pol-divider" />
<div class="pol-info-item" style="margin-bottom: 10px;">
<span class="pol-label">Phone</span>
<span class="pol-value">{{ policy.titular.phone }}</span>
</div>
<div class="pol-info-item" style="margin-bottom: 10px;">
<span class="pol-label">Email</span>
<span class="pol-value">{{ policy.titular.email }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Agent</span>
<span class="pol-value">{{ policy.agent }}</span>
</div>
</div>
<!-- LOB-Specific: Auto -->
<template v-if="policy.lob === 'Auto' && policy.vehicle">
<div class="pol-card pol-card-padded pol-grid-span-2">
<div class="pol-card-title">
<UIcon name="i-heroicons-truck" class="w-4 h-4" style="color: #2563eb;" />
Vehicle Details
</div>
<div class="pol-info-grid">
<div class="pol-info-item">
<span class="pol-label">Make / Model</span>
<span class="pol-value pol-value-bold">{{ policy.vehicle.year }} {{ policy.vehicle.make }} {{ policy.vehicle.model }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Color</span>
<span class="pol-value">{{ policy.vehicle.color }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">VIN</span>
<span class="pol-value pol-mono">{{ policy.vehicle.vin }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Plate</span>
<span class="pol-value pol-mono">{{ policy.vehicle.plate }}</span>
</div>
</div>
<template v-if="policy.vehicle.isFleet">
<div class="pol-divider" />
<div class="pol-fleet-banner">
<UIcon name="i-heroicons-truck" class="w-4 h-4" />
<div>
<span class="pol-fleet-name">{{ policy.vehicle.fleetName }}</span>
<span class="pol-fleet-discount">Group discount: {{ policy.vehicle.groupDiscount }}</span>
</div>
</div>
</template>
</div>
</template>
<!-- LOB-Specific: Health -->
<template v-if="policy.lob === 'Health' && policy.dependents">
<div class="pol-card pol-card-flush pol-grid-span-2">
<div class="pol-card-head">
<span>Dependents</span>
<span class="pol-card-head-count">{{ policy.dependents.length }}</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">Name</th><th class="pol-th">Relationship</th><th class="pol-th">Date of Birth</th><th class="pol-th">Cédula</th>
</tr></thead>
<tbody>
<tr v-for="d in policy.dependents" :key="d.cedula" class="pol-row">
<td class="pol-td pol-value-bold">{{ d.name }}</td>
<td class="pol-td">{{ d.relationship }}</td>
<td class="pol-td">{{ fmtDate(d.dob) }}</td>
<td class="pol-td pol-mono">{{ d.cedula }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- LOB-Specific: Life -->
<template v-if="policy.lob === 'Life' && policy.beneficiaries">
<div class="pol-card pol-card-flush pol-grid-span-2">
<div class="pol-card-head">
<span>Beneficiaries</span>
<span class="pol-card-head-count">{{ policy.beneficiaries.length }}</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">Name</th><th class="pol-th">Relationship</th><th class="pol-th">Type</th><th class="pol-th pol-th-right">%</th>
</tr></thead>
<tbody>
<tr v-for="b in policy.beneficiaries" :key="b.name" class="pol-row">
<td class="pol-td pol-value-bold">{{ b.name }}</td>
<td class="pol-td">{{ b.relationship }}</td>
<td class="pol-td"><span class="pol-status-badge" :class="b.type === 'primary' ? 'pol-status-active' : 'pol-status-cancelled'">{{ b.type }}</span></td>
<td class="pol-td pol-td-right pol-value-bold">{{ b.percentage }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="policy.mortgageHolder" class="pol-card pol-card-padded pol-grid-span-2">
<div class="pol-card-title">Mortgage Holder</div>
<div class="pol-info-grid">
<div class="pol-info-item"><span class="pol-label">Company</span><span class="pol-value">{{ policy.mortgageHolder.company }}</span></div>
<div class="pol-info-item"><span class="pol-label">Loan Number</span><span class="pol-value pol-mono">{{ policy.mortgageHolder.loanNumber }}</span></div>
<div class="pol-info-item"><span class="pol-label">Type</span><span class="pol-value">{{ policy.mortgageHolder.type }}</span></div>
</div>
</div>
</template>
<!-- LOB-Specific: General Risk / Property -->
<template v-if="policy.lob === 'General Risk' && policy.property">
<div class="pol-card pol-card-padded pol-grid-span-2">
<div class="pol-card-title">
<UIcon name="i-heroicons-building-office-2" class="w-4 h-4" style="color: #d97706;" />
Property Details
</div>
<div class="pol-info-grid">
<div class="pol-info-item"><span class="pol-label">Address</span><span class="pol-value">{{ policy.property.address }}</span></div>
<div class="pol-info-item"><span class="pol-label">Type</span><span class="pol-value">{{ policy.property.type }}</span></div>
<div class="pol-info-item"><span class="pol-label">Construction Year</span><span class="pol-value">{{ policy.property.constructionYear }}</span></div>
<div class="pol-info-item"><span class="pol-label">Area</span><span class="pol-value">{{ policy.property.area }}</span></div>
<div class="pol-info-item"><span class="pol-label">Insured Value</span><span class="pol-value pol-value-bold">{{ fmtMoney(policy.property.value) }}</span></div>
</div>
</div>
</template>
<!-- Endorsements -->
<div v-if="policy.endorsements.length > 0" class="pol-card pol-card-flush pol-grid-span-2">
<div class="pol-card-head">
<span>Endorsements</span>
<span class="pol-card-head-count">{{ policy.endorsements.length }}</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">ID</th><th class="pol-th">Date</th><th class="pol-th">Type</th><th class="pol-th">Description</th><th class="pol-th pol-th-right">Premium Impact</th>
</tr></thead>
<tbody>
<tr v-for="e in policy.endorsements" :key="e.id" class="pol-row">
<td class="pol-td pol-mono">{{ e.id }}</td>
<td class="pol-td">{{ fmtDate(e.date) }}</td>
<td class="pol-td"><span class="pol-endorse-type">{{ e.type }}</span></td>
<td class="pol-td">{{ e.description }}</td>
<td class="pol-td pol-td-right pol-value-bold" style="color: #d97706;">+{{ fmtMoney(e.premium) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Billing & Payments -->
<div class="pol-card pol-card-padded">
<div class="pol-card-title">
<UIcon name="i-heroicons-banknotes" class="w-4 h-4" style="color: #01696f;" />
Billing & Payments
</div>
<div class="pol-info-grid">
<div class="pol-info-item">
<span class="pol-label">Outstanding Balance</span>
<span class="pol-value pol-value-bold" :style="policy.billing.outstandingBalance === 0 ? 'color: #059669;' : 'color: #e11d48;'">
{{ fmtMoney(policy.billing.outstandingBalance) }}
<span v-if="policy.billing.outstandingBalance === 0" class="pol-billing-current-tag">Current</span>
</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Last Payment</span>
<span class="pol-value">{{ fmtDate(policy.billing.lastPaymentDate) }} {{ fmtMoney(policy.billing.lastPaymentAmount) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Payment Method</span>
<span class="pol-value">{{ policy.billing.paymentMethod }}</span>
</div>
</div>
<div class="pol-divider" />
<div class="pol-label" style="margin-bottom: 8px;">Recent Payments</div>
<div class="pol-table-wrap">
<table class="pol-table pol-table-compact">
<thead><tr>
<th class="pol-th">Date</th><th class="pol-th pol-th-right">Amount</th><th class="pol-th">Method</th><th class="pol-th">Status</th>
</tr></thead>
<tbody>
<tr v-for="p in policy.billing.paymentHistory" :key="p.date" class="pol-row">
<td class="pol-td">{{ fmtDate(p.date) }}</td>
<td class="pol-td pol-td-right pol-value-bold">{{ fmtMoney(p.amount) }}</td>
<td class="pol-td">{{ p.method }}</td>
<td class="pol-td"><span class="pol-status-badge pol-status-pill pol-status-active">{{ p.status }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Commission -->
<div class="pol-card pol-card-padded">
<div class="pol-card-title">
<UIcon name="i-heroicons-currency-dollar" class="w-4 h-4" style="color: #01696f;" />
Commission
</div>
<div class="pol-commission-highlight">
<span class="pol-commission-amount">{{ fmtMoney(policy.commission.amount) }}</span>
<span class="pol-commission-rate">{{ policy.commission.rate }}% of {{ fmtMoney(policy.annualPremium) }}</span>
</div>
<div class="pol-divider" />
<div class="pol-info-grid">
<div class="pol-info-item">
<span class="pol-label">Commission Rate</span>
<span class="pol-value pol-value-bold">{{ policy.commission.rate }}%</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Annual Amount</span>
<span class="pol-value pol-value-bold">{{ fmtMoney(policy.commission.amount) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Status</span>
<span class="pol-status-badge pol-status-pill pol-status-active">{{ policy.commission.status }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Producer / Agent of Record</span>
<span class="pol-value">{{ policy.commission.producer }}</span>
</div>
</div>
<!-- Agent Payout Breakdown -->
<div class="pol-divider" />
<div class="pol-card-title" style="margin-top: 4px;">
<UIcon name="i-heroicons-banknotes" class="w-4 h-4" style="color: #01696f;" />
Agent Payout
</div>
<div class="pol-agent-payout">
<div class="pol-payout-bar">
<div class="pol-payout-paid" :style="{ width: (policy.commission.paid / policy.commission.earned * 100) + '%' }" />
</div>
<div class="pol-payout-legend">
<span class="pol-payout-legend-item"><span class="pol-payout-dot pol-payout-dot-paid" /> Paid {{ fmtMoney(policy.commission.paid) }}</span>
<span class="pol-payout-legend-item"><span class="pol-payout-dot pol-payout-dot-owed" /> Owed {{ fmtMoney(policy.commission.owed) }}</span>
</div>
</div>
<div class="pol-info-grid" style="margin-top: 12px;">
<div class="pol-info-item">
<span class="pol-label">Total Earned</span>
<span class="pol-value pol-value-bold">{{ fmtMoney(policy.commission.earned) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Paid to Agent</span>
<span class="pol-value" style="color: #059669;">{{ fmtMoney(policy.commission.paid) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Amount Owed</span>
<span class="pol-value pol-value-bold" style="color: #d97706;">{{ fmtMoney(policy.commission.owed) }}</span>
</div>
<div class="pol-info-item">
<span class="pol-label">Next Payment Date</span>
<span class="pol-value">{{ fmtDate(policy.commission.nextPaymentDate) }}</span>
</div>
</div>
</div>
<!-- Related Policies -->
<div v-if="policy.economicGroup" class="pol-card pol-card-flush pol-grid-span-2">
<div class="pol-card-head">
<div style="display: flex; align-items: center; gap: 8px;">
<UIcon name="i-heroicons-building-library" class="w-4 h-4" style="color: #01696f;" />
<span>{{ policy.economicGroup }}</span>
</div>
<span class="pol-card-head-count">{{ policy.relatedPolicies.length }} related</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">Policy #</th><th class="pol-th">LOB</th><th class="pol-th">Titular</th><th class="pol-th">Status</th>
</tr></thead>
<tbody>
<tr v-for="rp in policy.relatedPolicies" :key="rp.id" class="pol-row" style="cursor: pointer;">
<td class="pol-td"><NuxtLink :to="`/policies/${rp.id}`" class="pol-customer-link">{{ rp.policyNumber }}</NuxtLink></td>
<td class="pol-td">{{ rp.lob }}</td>
<td class="pol-td">{{ rp.titular }}</td>
<td class="pol-td"><span class="pol-status-badge pol-status-pill" :class="statusClass(rp.status)">{{ statusLabel(rp.status) }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<!-- CLAIMS TAB -->
<template v-if="activeTab === 'claims'">
<div class="pol-card pol-card-flush">
<div class="pol-card-head">
<span>Claims History</span>
<span class="pol-card-head-count">{{ policy.claims.length }}</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">Claim ID</th><th class="pol-th">Date Filed</th><th class="pol-th">Type</th><th class="pol-th">Status</th><th class="pol-th pol-th-right">Amount</th><th class="pol-th pol-th-actions" />
</tr></thead>
<tbody>
<template v-for="c in policy.claims" :key="c.id">
<tr class="pol-row pol-row-clickable" @click="toggleClaim(c.id)">
<td class="pol-td"><NuxtLink :to="`/claims/${c.id}`" class="pol-policy-link pol-mono" @click.stop>{{ c.id }}</NuxtLink></td>
<td class="pol-td">{{ fmtDate(c.date) }}</td>
<td class="pol-td">{{ c.type }}</td>
<td class="pol-td"><span class="pol-status-badge pol-status-pill" :class="statusClass(c.status)">{{ c.status }}</span></td>
<td class="pol-td pol-td-right pol-value-bold">{{ fmtMoney(c.amount) }}</td>
<td class="pol-td pol-td-actions">
<UIcon :name="expandedClaim === c.id ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="w-4 h-4" style="color: #8a8a86;" />
</td>
</tr>
<tr v-if="expandedClaim === c.id" class="pol-claim-expand-row">
<td colspan="6" class="pol-claim-expand-cell">
<p class="pol-claim-desc">{{ c.description }}</p>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- RENEWALS TAB -->
<template v-if="activeTab === 'renewals'">
<div class="pol-card pol-card-flush">
<div class="pol-card-head">
<span>Renewal History</span>
<span class="pol-card-head-count">{{ policy.renewals.length }} periods</span>
</div>
<div class="pol-table-wrap">
<table class="pol-table">
<thead><tr>
<th class="pol-th">Period</th><th class="pol-th pol-th-right">Premium</th><th class="pol-th pol-th-right">Change</th><th class="pol-th">Status</th>
</tr></thead>
<tbody>
<tr v-for="(r, idx) in policy.renewals" :key="r.period" class="pol-row">
<td class="pol-td pol-value-bold">{{ r.period }}</td>
<td class="pol-td pol-td-right pol-value-bold">{{ fmtMoney(r.premium) }}</td>
<td class="pol-td pol-td-right">
<span v-if="renewalChange(idx) !== null" class="pol-renewal-change" :class="Number(renewalChange(idx)) > 0 ? 'pol-change-up' : 'pol-change-down'">
{{ Number(renewalChange(idx)) > 0 ? '+' : '' }}{{ renewalChange(idx) }}%
</span>
<span v-else class="pol-value" style="color: #8a8a86;"></span>
</td>
<td class="pol-td"><span class="pol-status-badge pol-status-pill" :class="statusClass(r.status)">{{ r.status }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- DOCUMENTS TAB -->
<template v-if="activeTab === 'documents'">
<div class="pol-docs-grid">
<div v-for="doc in policy.documents" :key="doc.id" class="pol-card pol-card-padded pol-doc-card">
<div class="pol-doc-row">
<div class="pol-doc-icon-wrap">
<UIcon name="i-heroicons-document-text" class="w-5 h-5" />
</div>
<div class="pol-doc-info">
<div class="pol-doc-name">{{ doc.name }}</div>
<div class="pol-doc-meta">
<span class="pol-doc-type-badge" :class="docTypeClass(doc.type)">{{ doc.type }}</span>
<span>{{ fmtDate(doc.date) }}</span>
<span class="pol-detail-sep">·</span>
<span>{{ doc.size }}</span>
</div>
</div>
<button class="pol-btn-secondary pol-btn-sm">
<UIcon name="i-heroicons-arrow-down-tray" class="w-3.5 h-3.5" />
Download
</button>
</div>
</div>
</div>
</template>
<!-- VIEW POLICY TAB -->
<template v-if="activeTab === 'view-policy'">
<div class="pol-vd-layout">
<!-- Sidebar -->
<div class="pol-vd-sidebar">
<template v-for="group in viewableDocGroups" :key="group.label">
<template v-if="group.items.length">
<div class="pol-vd-group-label">{{ group.label }}</div>
<button
v-for="doc in group.items"
:key="doc.id"
class="pol-vd-card"
:class="selectedDoc.id === doc.id ? 'pol-vd-card-active' : ''"
@click="selectedDoc = doc"
>
<div class="pol-vd-card-name">{{ doc.name }}</div>
<div class="pol-vd-card-meta">
<span class="pol-vd-type-badge" :class="docTypeBadgeClass(doc.type)">{{ docTypeBadgeLabel(doc.type) }}</span>
<span class="pol-vd-pages">{{ doc.pages }} {{ doc.pages === 1 ? 'page' : 'pages' }}</span>
</div>
</button>
</template>
</template>
</div>
<!-- Viewer Panel -->
<div class="pol-vd-viewer">
<!-- Toolbar -->
<div class="pol-vd-toolbar">
<div class="pol-vd-toolbar-left">
<UIcon name="i-heroicons-document-text" class="w-4 h-4" style="color: #01696f;" />
<span class="pol-vd-toolbar-name">{{ selectedDoc.name }}</span>
</div>
<div class="pol-vd-toolbar-center">
<span class="pol-vd-page-indicator">Page 1 of {{ selectedDoc.pages }}</span>
</div>
<div class="pol-vd-toolbar-right">
<button class="pol-vd-zoom-btn" title="Zoom out">
<UIcon name="i-heroicons-minus" class="w-3.5 h-3.5" />
</button>
<span class="pol-vd-zoom-level">100%</span>
<button class="pol-vd-zoom-btn" title="Zoom in">
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
</button>
<div class="pol-vd-toolbar-sep" />
<button class="pol-btn-secondary pol-btn-sm">
<UIcon name="i-heroicons-arrow-down-tray" class="w-3.5 h-3.5" />
Download
</button>
</div>
</div>
<!-- PDF Placeholder -->
<div class="pol-vd-pdf-area">
<div class="pol-vd-pdf-paper">
<UIcon name="i-heroicons-document-text" class="pol-vd-pdf-icon" />
<div class="pol-vd-pdf-label">PDF preview</div>
<div class="pol-vd-pdf-docname">{{ selectedDoc.name }}</div>
<div class="pol-vd-pdf-pages">{{ selectedDoc.pages }} {{ selectedDoc.pages === 1 ? 'page' : 'pages' }}</div>
</div>
</div>
</div>
</div>
</template>
<!-- ACTIVITY TAB -->
<template v-if="activeTab === 'activity'">
<div class="pol-card pol-card-padded">
<div class="pol-card-title" style="margin-bottom: 16px;">Activity Timeline</div>
<div class="pol-timeline">
<div v-for="(a, idx) in policy.activity" :key="idx" class="pol-timeline-item">
<div class="pol-timeline-line-wrap">
<div class="pol-timeline-dot" />
<div v-if="idx < policy.activity.length - 1" class="pol-timeline-line" />
</div>
<div class="pol-timeline-content">
<div class="pol-timeline-header">
<span class="pol-timeline-action">{{ a.action }}</span>
<span class="pol-timeline-date">{{ fmtDate(a.date) }}</span>
</div>
<div class="pol-timeline-detail">{{ a.detail }}</div>
<div class="pol-timeline-agent">
<span class="pol-agent-circle">{{ a.agent.split(' ').map(w => w[0]).join('').slice(0, 2) }}</span>
{{ a.agent }}
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<style scoped>
/* =====================================================================
POLICY DETAIL PAGE — scoped, pol- prefix
===================================================================== */
.pol-root {
--pol-brand: #01696f;
--pol-brand-soft: rgba(1, 105, 111, 0.06);
--pol-border: rgba(0, 0, 0, 0.06);
--pol-border-strong: rgba(0, 0, 0, 0.08);
--pol-muted: #8a8a86;
--pol-surface: #ffffff;
}
/* ---- Back link ---- */
.pol-back-link {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 500;
color: var(--pol-muted);
text-decoration: none;
margin-bottom: 12px;
transition: color 150ms ease;
}
.pol-back-link:hover { color: var(--pol-brand); }
/* ---- Detail header ---- */
.pol-detail-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.pol-detail-header-left { display: flex; flex-direction: column; gap: 6px; }
.pol-detail-header-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pol-detail-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pol-detail-title {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-primary, #1a1a1a);
line-height: 1;
}
.pol-detail-subtitle {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-muted, #5c5650);
}
.pol-detail-sep { color: var(--pol-muted); opacity: 0.5; }
/* ---- LOB chip ---- */
.pol-lob-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
padding: 2px 8px 2px 5px;
border-radius: 10px;
white-space: nowrap;
}
.pol-lob-auto { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
.pol-lob-health { background: rgba(236, 72, 153, 0.08); color: #db2777; }
.pol-lob-life { background: rgba(16, 185, 129, 0.08); color: #059669; }
.pol-lob-general-risk { background: rgba(245, 158, 11, 0.08); color: #d97706; }
/* ---- Main tabs ---- */
.pol-main-tabs {
display: inline-flex;
gap: 2px;
padding: 3px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.04);
margin-bottom: 20px;
}
.pol-main-tab {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 150ms ease;
white-space: nowrap;
}
.pol-main-tab-on {
background: #fff;
color: var(--text-primary, #1a1a1a);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.pol-main-tab-off {
background: transparent;
color: var(--pol-muted);
}
.pol-main-tab-off:hover { color: var(--text-primary, #1a1a1a); }
/* ---- Tab count badge ---- */
.pol-tab-count {
font-size: 9px;
font-weight: 600;
padding: 0px 5px;
border-radius: 9999px;
background: rgba(0, 0, 0, 0.06);
color: var(--pol-muted);
}
.pol-tab-count-on { background: rgba(1, 105, 111, 0.1); color: var(--pol-brand); }
/* ---- Cards ---- */
.pol-card {
background: var(--pol-surface);
border-radius: 12px;
border: 1px solid var(--pol-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.pol-card-padded { padding: 20px; }
.pol-card-flush { overflow: hidden; }
.pol-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
margin-bottom: 14px;
}
.pol-card-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid var(--pol-border);
font-size: 13px;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
}
.pol-card-head-count {
font-size: 11px;
font-weight: 600;
color: var(--pol-muted);
background: rgba(0, 0, 0, 0.04);
padding: 2px 8px;
border-radius: 10px;
}
/* ---- 2-col grid ---- */
.pol-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.pol-grid-span-2 { grid-column: span 2; }
/* ---- Info grid (label/value pairs) ---- */
.pol-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 24px;
}
.pol-info-item {
display: flex;
flex-direction: column;
gap: 3px;
}
.pol-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--pol-muted);
}
.pol-value {
font-size: 13px;
color: var(--text-primary, #1a1a1a);
}
.pol-value-bold { font-weight: 600; }
.pol-mono {
font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace;
font-size: 12px;
}
/* ---- Buttons ---- */
.pol-btn-primary {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
font-size: 12px;
font-weight: 600;
color: #ffffff;
background: var(--pol-brand);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 150ms ease;
}
.pol-btn-primary:hover { background: #015258; }
.pol-btn-secondary {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
font-size: 12px;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
background: #ffffff;
border: 1px solid var(--pol-border-strong);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
.pol-btn-secondary:hover {
border-color: rgba(1, 105, 111, 0.2);
color: var(--pol-brand);
}
.pol-btn-sm { padding: 5px 10px; font-size: 11px; }
/* ---- Status badges ---- */
.pol-status-badge {
display: inline-block;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
.pol-status-pill { border-radius: 9999px; }
.pol-status-active { background: rgba(16, 185, 129, 0.1); color: #059669; }
.pol-status-pending { background: rgba(245, 158, 11, 0.1); color: #d97706; }
.pol-status-lapsed { background: rgba(244, 63, 94, 0.1); color: #e11d48; }
.pol-status-cancelled { background: rgba(0, 0, 0, 0.05); color: var(--pol-muted); }
.pol-status-expired { background: rgba(249, 115, 22, 0.1); color: #ea580c; }
/* ---- Payment status ---- */
.pol-pay-badge {
display: inline-block;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 9999px;
white-space: nowrap;
}
.pol-pay-current { background: rgba(16, 185, 129, 0.1); color: #059669; }
.pol-pay-overdue { background: rgba(244, 63, 94, 0.1); color: #e11d48; }
.pol-pay-grace { background: rgba(245, 158, 11, 0.1); color: #d97706; }
/* ---- Divider ---- */
.pol-divider {
height: 1px;
background: var(--pol-border);
margin: 12px 0;
}
/* ---- Coverage ---- */
.pol-coverage-header {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.pol-coverage-list { display: flex; flex-direction: column; gap: 0; }
.pol-coverage-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
}
.pol-coverage-row:last-child { border-bottom: none; }
.pol-coverage-label { font-size: 13px; color: var(--text-muted, #5c5650); }
.pol-coverage-value { font-size: 13px; font-weight: 600; color: var(--text-primary, #1a1a1a); }
/* ---- Policyholder ---- */
.pol-holder-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.pol-holder-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--pol-brand-soft);
color: var(--pol-brand);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pol-holder-name { font-size: 14px; font-weight: 600; color: var(--text-primary, #1a1a1a); }
.pol-holder-cedula { font-size: 12px; color: var(--pol-muted); font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; }
/* ---- Fleet banner ---- */
.pol-fleet-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
background: rgba(59, 130, 246, 0.04);
border: 1px solid rgba(59, 130, 246, 0.1);
color: #2563eb;
}
.pol-fleet-name { font-size: 13px; font-weight: 600; display: block; }
.pol-fleet-discount { font-size: 11px; font-weight: 500; opacity: 0.8; display: block; margin-top: 1px; }
/* ---- Table ---- */
.pol-table-wrap { overflow-x: auto; }
.pol-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.pol-th {
padding: 8px 12px;
text-align: left;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--pol-muted);
border-bottom: 1px solid var(--pol-border);
white-space: nowrap;
}
.pol-th-right { text-align: right; }
.pol-th-actions { text-align: center; width: 48px; }
.pol-td {
padding: 8px 12px;
vertical-align: middle;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
font-size: 13px;
}
.pol-td-right { text-align: right; }
.pol-td-actions { text-align: center; white-space: nowrap; }
.pol-row { transition: background 120ms ease; }
.pol-row:hover { background: rgba(0, 0, 0, 0.015); }
.pol-row:last-child .pol-td { border-bottom: none; }
.pol-row-clickable { cursor: pointer; }
/* ---- Customer link ---- */
.pol-customer-link {
font-size: 13px;
font-weight: 500;
color: var(--pol-brand);
text-decoration: none;
transition: color 150ms ease;
}
.pol-customer-link:hover { color: #015258; text-decoration: underline; }
/* ---- Endorsement type ---- */
.pol-endorse-type {
display: inline-block;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 6px;
background: rgba(139, 92, 246, 0.08);
color: #7c3aed;
}
/* ---- Claims expand ---- */
.pol-claim-expand-row { background: rgba(0, 0, 0, 0.015); }
.pol-claim-expand-cell { padding: 12px 16px !important; border-bottom: 1px solid var(--pol-border); }
.pol-claim-desc { font-size: 13px; color: var(--text-muted, #5c5650); line-height: 1.5; margin: 0; }
/* ---- Renewal change ---- */
.pol-renewal-change { font-size: 12px; font-weight: 600; }
.pol-change-up { color: #e11d48; }
.pol-change-down { color: #059669; }
/* ---- Documents grid ---- */
.pol-docs-grid { display: flex; flex-direction: column; gap: 8px; }
.pol-doc-card { padding: 14px 18px !important; }
.pol-doc-row {
display: flex;
align-items: center;
gap: 14px;
}
.pol-doc-icon-wrap {
width: 38px;
height: 38px;
border-radius: 10px;
background: var(--pol-brand-soft);
color: var(--pol-brand);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pol-doc-info { flex: 1; min-width: 0; }
.pol-doc-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pol-doc-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--pol-muted);
margin-top: 3px;
}
.pol-doc-type-badge {
display: inline-block;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 4px;
}
.pol-doc-poliza { background: rgba(1, 105, 111, 0.08); color: var(--pol-brand); }
.pol-doc-endoso { background: rgba(139, 92, 246, 0.08); color: #7c3aed; }
.pol-doc-certificado { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
.pol-doc-recibo { background: rgba(245, 158, 11, 0.08); color: #d97706; }
.pol-doc-other { background: rgba(0, 0, 0, 0.05); color: var(--pol-muted); }
/* ---- Activity timeline ---- */
.pol-timeline { display: flex; flex-direction: column; }
.pol-timeline-item { display: flex; gap: 14px; }
.pol-timeline-line-wrap {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 16px;
}
.pol-timeline-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--pol-brand);
flex-shrink: 0;
margin-top: 4px;
}
.pol-timeline-line {
width: 2px;
flex: 1;
background: var(--pol-border);
min-height: 20px;
}
.pol-timeline-content {
flex: 1;
padding-bottom: 20px;
}
.pol-timeline-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.pol-timeline-action {
font-size: 13px;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
}
.pol-timeline-date {
font-size: 11px;
color: var(--pol-muted);
white-space: nowrap;
}
.pol-timeline-detail {
font-size: 13px;
color: var(--text-muted, #5c5650);
line-height: 1.5;
margin-bottom: 6px;
}
.pol-timeline-agent {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--pol-muted);
font-weight: 500;
}
.pol-agent-circle {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--pol-brand-soft);
color: var(--pol-brand);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
letter-spacing: 0.02em;
}
/* ---- Quick Info Strip ---- */
.pol-quick-strip {
display: flex;
align-items: center;
gap: 0;
padding: 12px 20px;
background: var(--pol-surface);
border-radius: 12px;
border: 1px solid var(--pol-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
margin-bottom: 20px;
}
.pol-quick-item {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.pol-quick-icon { color: var(--pol-brand); flex-shrink: 0; }
.pol-quick-data { display: flex; flex-direction: column; gap: 1px; }
.pol-quick-value {
font-size: 16px;
font-weight: 700;
color: var(--text-primary, #1a1a1a);
line-height: 1.2;
}
.pol-quick-warn { color: #e11d48; }
.pol-quick-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--pol-muted);
}
.pol-quick-sep {
width: 1px;
height: 32px;
background: var(--pol-border);
margin: 0 16px;
flex-shrink: 0;
}
/* ---- Billing current tag ---- */
.pol-billing-current-tag {
display: inline-block;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 4px;
background: rgba(16, 185, 129, 0.1);
color: #059669;
margin-left: 6px;
vertical-align: middle;
}
/* ---- Compact table ---- */
.pol-table-compact .pol-th { padding: 6px 10px; font-size: 10px; }
.pol-table-compact .pol-td { padding: 6px 10px; font-size: 12px; }
/* ---- Commission highlight ---- */
.pol-commission-highlight {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 16px;
border-radius: 8px;
background: rgba(1, 105, 111, 0.04);
border: 1px solid rgba(1, 105, 111, 0.08);
margin-bottom: 4px;
}
.pol-commission-amount {
font-size: 22px;
font-weight: 700;
color: var(--pol-brand);
line-height: 1;
}
.pol-commission-rate {
font-size: 11px;
font-weight: 500;
color: var(--pol-muted);
}
/* ---- Agent Payout ---- */
.pol-agent-payout { padding: 0 0 4px; }
.pol-payout-bar {
height: 8px;
border-radius: 4px;
background: rgba(217, 119, 6, 0.15);
overflow: hidden;
margin-bottom: 8px;
}
.pol-payout-paid {
height: 100%;
border-radius: 4px;
background: #059669;
transition: width 0.3s ease;
}
.pol-payout-legend {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--pol-text);
}
.pol-payout-legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.pol-payout-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.pol-payout-dot-paid { background: #059669; }
.pol-payout-dot-owed { background: #d97706; }
/* ---- View Policy (PDF Viewer) Tab ---- */
.pol-vd-layout {
display: flex;
gap: 0;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
overflow: hidden;
background: #fff;
min-height: 600px;
}
.pol-vd-sidebar {
width: 280px;
flex-shrink: 0;
padding: 16px;
overflow-y: auto;
background: #fafaf9;
border-right: 1px solid rgba(0, 0, 0, 0.06);
}
.pol-vd-group-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8a8a86;
margin: 16px 0 6px;
}
.pol-vd-group-label:first-child {
margin-top: 0;
}
.pol-vd-card {
display: block;
width: 100%;
text-align: left;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.15s ease;
}
.pol-vd-card:hover {
border-color: rgba(1, 105, 111, 0.25);
background: rgba(1, 105, 111, 0.02);
}
.pol-vd-card-active {
border-color: #01696f;
background: rgba(1, 105, 111, 0.06);
box-shadow: 0 0 0 1px rgba(1, 105, 111, 0.15);
}
.pol-vd-card-name {
font-size: 12.5px;
font-weight: 500;
color: #1a1a18;
line-height: 1.3;
margin-bottom: 6px;
}
.pol-vd-card-meta {
display: flex;
align-items: center;
gap: 8px;
}
.pol-vd-type-badge {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 1px 6px;
border-radius: 4px;
}
.pol-vd-badge-policy { background: rgba(1, 105, 111, 0.1); color: #01696f; }
.pol-vd-badge-endorsement { background: rgba(168, 85, 247, 0.1); color: #7c3aed; }
.pol-vd-badge-certificate { background: rgba(16, 185, 129, 0.1); color: #059669; }
.pol-vd-badge-related { background: rgba(59, 130, 246, 0.1); color: #2563eb; }
.pol-vd-pages {
font-size: 11px;
color: #8a8a86;
}
/* Viewer panel */
.pol-vd-viewer {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.pol-vd-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: #fafaf9;
gap: 12px;
}
.pol-vd-toolbar-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.pol-vd-toolbar-name {
font-size: 12.5px;
font-weight: 500;
color: #1a1a18;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pol-vd-toolbar-center {
flex-shrink: 0;
}
.pol-vd-page-indicator {
font-size: 12px;
color: #8a8a86;
font-weight: 500;
}
.pol-vd-toolbar-right {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.pol-vd-zoom-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
color: #6b6b67;
cursor: pointer;
transition: all 0.15s ease;
}
.pol-vd-zoom-btn:hover {
border-color: rgba(0, 0, 0, 0.12);
color: #1a1a18;
}
.pol-vd-zoom-level {
font-size: 11.5px;
font-weight: 500;
color: #6b6b67;
min-width: 36px;
text-align: center;
}
.pol-vd-toolbar-sep {
width: 1px;
height: 20px;
background: rgba(0, 0, 0, 0.08);
margin: 0 4px;
}
/* PDF area */
.pol-vd-pdf-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background: #f0f0ee;
}
.pol-vd-pdf-paper {
width: 100%;
max-width: 520px;
min-height: 400px;
background: #fff;
border-radius: 4px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.08),
0 4px 12px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px 24px;
}
.pol-vd-pdf-icon {
width: 48px;
height: 48px;
color: #c4c4c0;
}
.pol-vd-pdf-label {
font-size: 14px;
font-weight: 600;
color: #8a8a86;
letter-spacing: 0.02em;
}
.pol-vd-pdf-docname {
font-size: 12.5px;
color: #6b6b67;
text-align: center;
max-width: 360px;
line-height: 1.4;
}
.pol-vd-pdf-pages {
font-size: 11px;
color: #a3a3a0;
margin-top: 4px;
}
/* ---- Responsive ---- */
@media (max-width: 768px) {
.pol-grid-2 { grid-template-columns: 1fr; }
.pol-grid-span-2 { grid-column: span 1; }
.pol-info-grid { grid-template-columns: 1fr; }
.pol-detail-header { flex-direction: column; }
.pol-detail-header-right { width: 100%; }
.pol-main-tabs { overflow-x: auto; width: 100%; }
.pol-quick-strip { flex-wrap: wrap; gap: 12px; }
.pol-quick-sep { display: none; }
.pol-quick-item { flex: 0 0 calc(50% - 6px); }
.pol-vd-layout { flex-direction: column; }
.pol-vd-sidebar { width: 100%; border-right: none; border-bottom: 1px solid rgba(0, 0, 0, 0.06); max-height: 240px; }
}
</style>