WIP jordan

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

837
app/pages/renewals/[id].vue Normal file
View File

@@ -0,0 +1,837 @@
<script setup lang="ts">
import {
MOCK_RENEWAL_DETAILS, carrierStatusLabels, brokerStatusLabels,
priorityLabels, retentionRiskLabels, slaColor,
type RenewalDetail, type RenewalTask, type RenewalCommunication,
type CarrierRenewalStatus, type BrokerRenewalStatus, type RenewalPriority,
type RetentionRisk, type CoverageLine,
} from '~/data/mock-renewals'
definePageMeta({ ssr: false })
usePageTitle('Renewal Detail')
const route = useRoute()
const renewalId = route.params.id as string
const renewal = ref<RenewalDetail | null>(MOCK_RENEWAL_DETAILS[renewalId] ?? null)
// ── Tabs ──────────────────────────────────────────────────────────────────────
type TabId = 'overview' | 'tasks' | 'communications' | 'documents' | 'comparison'
const activeTab = ref<TabId>('overview')
const tabs: { id: TabId; label: string; icon: string }[] = [
{ id: 'overview', label: 'Overview', icon: 'i-heroicons-squares-2x2' },
{ id: 'tasks', label: 'Tasks', icon: 'i-heroicons-clipboard-document-check' },
{ id: 'communications', label: 'Communications', icon: 'i-heroicons-chat-bubble-left-right' },
{ id: 'documents', label: 'Documents', icon: 'i-heroicons-folder-open' },
{ id: 'comparison', label: 'Comparison', icon: 'i-heroicons-arrows-right-left' },
]
// ── Helpers ────────────────────────────────────────────────────────────────────
const fmtMoney = (n: number) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 0 })
const fmtDate = (d: string) => {
if (!d) return '—'
return new Date(d).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })
}
const fmtDateTime = (d: string) => {
if (!d) return '—'
return new Date(d).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })
}
const carrierPillClass = (s: CarrierRenewalStatus) => {
const map: Record<string, string> = {
pending: 'rnd-csp-pending', terms_received: 'rnd-csp-terms', remarketing: 'rnd-csp-remarket',
bound: 'rnd-csp-bound', declined: 'rnd-csp-declined', lapsed: 'rnd-csp-lapsed',
}
return map[s] ?? ''
}
const priorityClass = (p: RenewalPriority) => {
const map: Record<string, string> = { critical: 'rnd-pri-critical', high: 'rnd-pri-high', medium: 'rnd-pri-medium', low: 'rnd-pri-low' }
return map[p] ?? ''
}
const riskClass = (r: RetentionRisk) => {
const map: Record<string, string> = { high: 'rnd-risk-high', medium: 'rnd-risk-medium', low: 'rnd-risk-low' }
return map[r] ?? ''
}
const taskStatusClass = (s: string) => {
const map: Record<string, string> = { open: 'rnd-ts-open', in_progress: 'rnd-ts-progress', overdue: 'rnd-ts-overdue', done: 'rnd-ts-done' }
return map[s] ?? ''
}
const commIcon = (t: string) => {
const map: Record<string, string> = { email: 'i-heroicons-envelope', call: 'i-heroicons-phone', note: 'i-heroicons-pencil-square', system: 'i-heroicons-cog-6-tooth' }
return map[t] ?? 'i-heroicons-chat-bubble-left'
}
const deltaFlagClass = (flag: CoverageLine['flag']) => {
const map: Record<string, string> = { increase: 'rnd-delta-up', decrease: 'rnd-delta-down', same: '', new: 'rnd-delta-new', removed: 'rnd-delta-removed' }
return map[flag ?? ''] ?? ''
}
// ── Party filter ─────────────────────────────────────────────────────────────
const activePartyId = ref<string | null>(null)
const commTypeFilter = ref<string | null>(null)
const filteredComms = computed(() => {
if (!renewal.value) return []
let comms = [...renewal.value.communications]
if (activePartyId.value) {
const party = renewal.value.parties.find(p => p.id === activePartyId.value)
if (party) comms = comms.filter(c => c.partyRole === party.role)
}
if (commTypeFilter.value) comms = comms.filter(c => c.type === commTypeFilter.value)
return comms.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
})
// ── Tasks ─────────────────────────────────────────────────────────────────────
const sortedTasks = computed(() => {
if (!renewal.value) return []
return [...renewal.value.tasks].sort((a, b) => {
const order: Record<string, number> = { overdue: 0, open: 1, in_progress: 2, done: 3 }
return (order[a.status] ?? 9) - (order[b.status] ?? 9)
})
})
const openActions = computed(() => {
if (!renewal.value) return []
return renewal.value.tasks.filter(t => t.status === 'open' || t.status === 'overdue' || t.status === 'in_progress')
})
// ── AI ────────────────────────────────────────────────────────────────────────
const aiBriefOpen = ref(true)
// ── Quick compose ─────────────────────────────────────────────────────────────
const composeText = ref('')
const composeType = ref<'email' | 'call' | 'note'>('note')
// ── Action handlers ──────────────────────────────────────────────────────────
const toast = useToast()
function handleSendProposal() {
toast.add({ title: 'Proposal sent', description: `Renewal proposal sent to ${renewal.value?.customerName}.`, color: 'green' })
}
function handleSendFollowUp() {
toast.add({ title: 'Follow-up sent', description: 'Reminder email queued for delivery.', color: 'green' })
}
// ── Days display ─────────────────────────────────────────────────────────────
function daysDisplay(d: number) {
if (d < 0) return `${Math.abs(d)}d past due`
if (d === 0) return 'Expires today'
return `${d}d until expiry`
}
function daysClass(d: number) {
if (d < 0) return 'rnd-days-past'
if (d <= 7) return 'rnd-days-critical'
if (d <= 30) return 'rnd-days-warn'
return ''
}
// ── Renewal history max ──────────────────────────────────────────────────────
const maxHistoryPremium = computed(() => {
if (!renewal.value) return 1
return Math.max(...renewal.value.renewalHistory.map(h => h.premium), 1)
})
</script>
<template>
<div class="rnd-root mx-auto max-w-6xl pb-12">
<!-- Not found -->
<template v-if="!renewal">
<div class="rnd-not-found">
<UIcon name="i-heroicons-exclamation-triangle" class="w-8 h-8" />
<h2>Renewal not found</h2>
<p>No renewal with ID <strong>{{ renewalId }}</strong> exists.</p>
<NuxtLink to="/renewals" class="rnd-back-link">Back to Renewals</NuxtLink>
</div>
</template>
<template v-else>
<!-- Back link -->
<NuxtLink to="/renewals" class="rnd-back-link">
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
Back to Renewals
</NuxtLink>
<!-- HEADER -->
<div class="rnd-detail-header">
<div class="rnd-header-left">
<div class="rnd-title-row">
<h1 class="rnd-title">{{ renewal.id }}</h1>
<span class="rnd-carrier-pill" :class="carrierPillClass(renewal.carrierStatus)">
{{ carrierStatusLabels[renewal.carrierStatus] }}
</span>
<span class="rnd-workflow-pill">
{{ brokerStatusLabels[renewal.brokerStatus] }}
</span>
<span class="rnd-priority-badge" :class="priorityClass(renewal.priority)">
{{ priorityLabels[renewal.priority] }}
</span>
<span class="rnd-risk-badge" :class="riskClass(renewal.retentionRisk)">
Risk: {{ retentionRiskLabels[renewal.retentionRisk] }}
</span>
</div>
<div class="rnd-subtitle">
<NuxtLink :to="`/customers/${renewal.customerId}`" class="rnd-subtitle-link">{{ renewal.customerName }}</NuxtLink>
<span class="rnd-sep">/</span>
<NuxtLink :to="`/policies/${renewal.policyId}`" class="rnd-subtitle-link">{{ renewal.policyNumber }}</NuxtLink>
<span class="rnd-sep">/</span>
<span>{{ renewal.carrier }}</span>
<span class="rnd-sep">/</span>
<span>{{ renewal.lob }}</span>
</div>
<div class="rnd-meta-row">
<span :class="daysClass(renewal.daysUntilExpiry)">{{ daysDisplay(renewal.daysUntilExpiry) }}</span>
<span class="rnd-sep">·</span>
<span>Expires {{ fmtDate(renewal.expiryDate) }}</span>
<span class="rnd-sep">·</span>
<span>Handler: <strong>{{ renewal.assignedTo }}</strong></span>
<span class="rnd-sep">·</span>
<span>{{ renewal.yearsAsClient }}yr client</span>
</div>
</div>
<div class="rnd-header-right">
<button v-if="renewal.brokerStatus === 'under_review' || renewal.brokerStatus === 'unreviewed'" class="rnd-action-btn" @click="handleSendProposal">
<UIcon name="i-heroicons-paper-airplane" class="w-4 h-4" />
Send Proposal
</button>
<button v-else-if="renewal.brokerStatus === 'awaiting_client_response'" class="rnd-action-btn" @click="handleSendFollowUp">
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4" />
Send Follow-Up
</button>
<button v-else-if="renewal.brokerStatus === 'awaiting_payment'" class="rnd-action-btn" @click="toast.add({ title: 'Payment reminder sent', color: 'green' })">
<UIcon name="i-heroicons-banknotes" class="w-4 h-4" />
Collect Payment
</button>
</div>
</div>
<!-- PARTIES STRIP -->
<div class="rnd-party-strip">
<button class="rnd-party-chip" :class="{ 'rnd-party-active': !activePartyId }" @click="activePartyId = null">
<span class="rnd-party-avatar">ALL</span>
<span class="rnd-party-name">All Parties</span>
</button>
<button
v-for="party in renewal.parties"
:key="party.id"
class="rnd-party-chip"
:class="{ 'rnd-party-active': activePartyId === party.id }"
@click="activePartyId = activePartyId === party.id ? null : party.id"
>
<span class="rnd-party-avatar">{{ party.name.split(' ').map(w => w[0]).join('').slice(0, 2) }}</span>
<div class="rnd-party-info">
<span class="rnd-party-role">{{ party.role.replace('_', ' ') }}</span>
<span class="rnd-party-name">{{ party.name }}</span>
</div>
<span v-if="party.hasUnread" class="rnd-party-dot" />
</button>
<button class="rnd-party-chip rnd-party-add">
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
Add Party
</button>
</div>
<!-- QUICK STATS -->
<div class="rnd-quick-strip">
<div class="rnd-quick-item">
<UIcon name="i-heroicons-currency-dollar" class="w-4 h-4 rnd-quick-icon" />
<div class="rnd-quick-data">
<span class="rnd-quick-value">{{ fmtMoney(renewal.currentPremium) }}</span>
<span class="rnd-quick-label">Current</span>
</div>
</div>
<div class="rnd-quick-sep" />
<div class="rnd-quick-item">
<UIcon name="i-heroicons-arrow-trending-up" class="w-4 h-4 rnd-quick-icon" />
<div class="rnd-quick-data">
<span class="rnd-quick-value">
<template v-if="renewal.renewalPremium !== null">
{{ fmtMoney(renewal.renewalPremium) }}
<span v-if="renewal.premiumDelta !== null" class="rnd-quick-delta" :class="renewal.premiumDelta > 0 ? 'rnd-delta-up' : 'rnd-delta-down'">
{{ renewal.premiumDelta > 0 ? '+' : '' }}{{ renewal.premiumDelta }}%
</span>
</template>
<template v-else></template>
</span>
<span class="rnd-quick-label">Renewal</span>
</div>
</div>
<div class="rnd-quick-sep" />
<div class="rnd-quick-item">
<UIcon name="i-heroicons-chart-bar" class="w-4 h-4 rnd-quick-icon" />
<div class="rnd-quick-data">
<span class="rnd-quick-value">{{ (renewal.lossRatio * 100).toFixed(0) }}%</span>
<span class="rnd-quick-label">Loss Ratio</span>
</div>
</div>
<div class="rnd-quick-sep" />
<div class="rnd-quick-item">
<UIcon name="i-heroicons-banknotes" class="w-4 h-4 rnd-quick-icon" />
<div class="rnd-quick-data">
<span class="rnd-quick-value">{{ fmtMoney(renewal.commissionAmount) }}</span>
<span class="rnd-quick-label">Commission ({{ renewal.commissionRate }}%)</span>
</div>
</div>
<div class="rnd-quick-sep" />
<div class="rnd-quick-item">
<UIcon name="i-heroicons-folder-open" class="w-4 h-4 rnd-quick-icon" />
<div class="rnd-quick-data">
<span class="rnd-quick-value">
{{ renewal.documents.filter(d => d.required && d.fulfilled).length }}/{{ renewal.documents.filter(d => d.required).length }}
</span>
<span class="rnd-quick-label">Docs</span>
</div>
</div>
</div>
<!-- OPEN ACTIONS -->
<div v-if="openActions.length" class="rnd-action-panel">
<div class="rnd-action-header">
<UIcon name="i-heroicons-bell-alert" class="w-4 h-4" />
Open Actions
<span class="rnd-action-count">{{ openActions.length }}</span>
</div>
<div v-for="task in openActions" :key="task.id" class="rnd-action-row">
<span class="rnd-sla-dot" :class="`rnd-sla-${slaColor(task.slaPercent)}`" />
<span class="rnd-action-title">{{ task.title }}</span>
<span class="rnd-action-assignee">{{ task.assignee }}</span>
<span class="rnd-action-due" :class="{ 'rnd-action-overdue': task.status === 'overdue' }">{{ fmtDate(task.dueDate) }}</span>
<div class="rnd-action-btns">
<button v-if="task.aiGenerated" class="rnd-btn-escalate">Escalate</button>
<button v-if="task.aiGenerated" class="rnd-btn-dismiss">Dismiss</button>
<button v-if="task.type === 'send_proposal'" class="rnd-btn-draft">Draft Email</button>
<button v-if="task.type === 'follow_up'" class="rnd-btn-draft">Send Reminder</button>
</div>
</div>
</div>
<!-- AI RENEWAL BRIEF -->
<div v-if="renewal.aiRenewalBrief" class="rnd-ai-panel">
<div class="rnd-ai-header" @click="aiBriefOpen = !aiBriefOpen">
<span class="rnd-ai-badge">IA</span>
<span class="rnd-ai-title">Renewal Brief</span>
<UIcon :name="aiBriefOpen ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="w-4 h-4 rnd-ai-toggle" />
</div>
<div v-if="aiBriefOpen" class="rnd-ai-body">
<p>{{ renewal.aiRenewalBrief }}</p>
<!-- Talk Track -->
<div v-if="renewal.aiTalkTrack && renewal.aiTalkTrack.length" class="rnd-ai-section">
<h4 class="rnd-ai-section-title">Talk Track</h4>
<ol class="rnd-ai-list">
<li v-for="(point, i) in renewal.aiTalkTrack" :key="i">{{ point }}</li>
</ol>
</div>
<!-- Retention Factors -->
<div v-if="renewal.aiRetentionFactors && renewal.aiRetentionFactors.length" class="rnd-ai-section">
<h4 class="rnd-ai-section-title">Retention Factors</h4>
<ul class="rnd-ai-list">
<li v-for="(factor, i) in renewal.aiRetentionFactors" :key="i">{{ factor }}</li>
</ul>
</div>
<button class="rnd-ai-regen">
<UIcon name="i-heroicons-arrow-path" class="w-3.5 h-3.5" />
Regenerar
</button>
</div>
</div>
<!-- TAB NAVIGATION -->
<div class="rnd-main-tabs">
<button
v-for="t in tabs"
:key="t.id"
type="button"
class="rnd-main-tab"
:class="activeTab === t.id ? 'rnd-main-tab-on' : 'rnd-main-tab-off'"
@click="activeTab = t.id"
>
<UIcon :name="t.icon" class="w-4 h-4" />
{{ t.label }}
</button>
</div>
<!-- OVERVIEW TAB -->
<template v-if="activeTab === 'overview'">
<!-- Renewal History -->
<div class="rnd-card">
<h3 class="rnd-card-title">Renewal History</h3>
<table class="rnd-table">
<thead>
<tr><th>Year</th><th>Carrier</th><th class="text-right">Premium</th><th>Outcome</th><th style="width: 40%;"></th></tr>
</thead>
<tbody>
<tr v-for="h in renewal.renewalHistory" :key="h.year">
<td class="font-semibold">{{ h.year }}</td>
<td>{{ h.carrier }}</td>
<td class="text-right font-semibold">{{ fmtMoney(h.premium) }}</td>
<td>
<span class="rnd-outcome" :class="`rnd-outcome-${h.outcome}`">{{ h.outcome }}</span>
</td>
<td>
<div class="rnd-bar-wrap">
<div class="rnd-bar" :style="{ width: (h.premium / maxHistoryPremium * 100) + '%' }" />
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Parties Detail -->
<div class="rnd-card">
<h3 class="rnd-card-title">Parties</h3>
<div class="rnd-parties-grid">
<div v-for="party in renewal.parties" :key="party.id" class="rnd-party-card">
<div class="rnd-party-card-avatar">{{ party.name.split(' ').map(w => w[0]).join('').slice(0, 2) }}</div>
<div>
<p class="rnd-party-card-name">{{ party.name }}</p>
<p class="rnd-party-card-role">{{ party.role.replace('_', ' ') }}<template v-if="party.company"> · {{ party.company }}</template></p>
<p v-if="party.email" class="rnd-party-card-contact">{{ party.email }}</p>
<p v-if="party.phone" class="rnd-party-card-contact">{{ party.phone }}</p>
</div>
</div>
</div>
</div>
<!-- Cancellation Data (if exists) -->
<div v-if="renewal.cancellationData" class="rnd-card rnd-card-cancel">
<h3 class="rnd-card-title" style="color: #c13838;">Cancellation Details</h3>
<div class="rnd-cancel-grid">
<div>
<span class="rnd-cancel-label">Reason</span>
<span class="rnd-cancel-value">{{ renewal.cancellationData.reason }}</span>
</div>
<div v-if="renewal.cancellationData.reasonDetail">
<span class="rnd-cancel-label">Detail</span>
<span class="rnd-cancel-value">{{ renewal.cancellationData.reasonDetail }}</span>
</div>
<div v-if="renewal.cancellationData.competitor">
<span class="rnd-cancel-label">Competitor</span>
<span class="rnd-cancel-value">{{ renewal.cancellationData.competitor }} @ {{ fmtMoney(renewal.cancellationData.competitorPremium!) }}</span>
</div>
<div>
<span class="rnd-cancel-label">Recoverable</span>
<span class="rnd-cancel-value" :style="renewal.cancellationData.recoverable ? 'color: #01696f;' : 'color: #c13838;'">{{ renewal.cancellationData.recoverable ? 'Yes' : 'No' }}</span>
</div>
<div>
<span class="rnd-cancel-label">Exit Date</span>
<span class="rnd-cancel-value">{{ fmtDate(renewal.cancellationData.exitDate) }}</span>
</div>
</div>
</div>
</template>
<!-- TASKS TAB -->
<template v-if="activeTab === 'tasks'">
<div class="rnd-card">
<table class="rnd-table">
<thead>
<tr>
<th style="width: 28px;"></th>
<th>Task</th>
<th>Type</th>
<th>Status</th>
<th>Assignee</th>
<th>Due</th>
<th style="width: 28px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="t in sortedTasks" :key="t.id">
<td><span class="rnd-sla-dot" :class="`rnd-sla-${slaColor(t.slaPercent)}`" /></td>
<td class="font-medium">{{ t.title }}</td>
<td class="text-[var(--text-muted)]">{{ t.type.replace('_', ' ') }}</td>
<td><span :class="taskStatusClass(t.status)">{{ t.status.replace('_', ' ') }}</span></td>
<td>{{ t.assignee }}</td>
<td :class="t.status === 'overdue' ? 'rnd-action-overdue' : ''">{{ fmtDate(t.dueDate) }}</td>
<td>
<UIcon v-if="t.aiGenerated" name="i-heroicons-sparkles" class="w-3.5 h-3.5" style="color: #01696f;" title="AI generated" />
</td>
</tr>
<tr v-if="!sortedTasks.length"><td colspan="7" class="rnd-table-empty">No tasks.</td></tr>
</tbody>
</table>
</div>
</template>
<!-- COMMUNICATIONS TAB -->
<template v-if="activeTab === 'communications'">
<!-- Type filters -->
<div class="rnd-comm-filters">
<button class="rnd-comm-chip" :class="!commTypeFilter ? 'rnd-comm-chip-on' : 'rnd-comm-chip-off'" @click="commTypeFilter = null">All</button>
<button v-for="ct in ['email', 'call', 'note', 'system']" :key="ct" class="rnd-comm-chip" :class="commTypeFilter === ct ? 'rnd-comm-chip-on' : 'rnd-comm-chip-off'" @click="commTypeFilter = commTypeFilter === ct ? null : ct">
<UIcon :name="commIcon(ct)" class="w-3.5 h-3.5" />
{{ ct }}
</button>
</div>
<!-- Timeline -->
<div class="rnd-timeline">
<div v-for="c in filteredComms" :key="c.id" class="rnd-timeline-item">
<div class="rnd-timeline-icon" :class="`rnd-ti-${c.type}`">
<UIcon :name="commIcon(c.type)" class="w-4 h-4" />
</div>
<div class="rnd-timeline-content">
<div class="rnd-timeline-head">
<span class="rnd-timeline-from">{{ c.from }}</span>
<span v-if="c.to" class="rnd-timeline-to"> {{ c.to }}</span>
<span class="rnd-timeline-dir">{{ c.direction }}</span>
<span v-if="c.templateUsed" class="rnd-timeline-tpl">template</span>
<span class="rnd-timeline-time">{{ fmtDateTime(c.timestamp) }}</span>
</div>
<p v-if="c.subject" class="rnd-timeline-subject">{{ c.subject }}</p>
<p class="rnd-timeline-body">{{ c.body }}</p>
<div v-if="c.aiDigest" class="rnd-ai-digest">
<span class="rnd-ai-badge" style="font-size: 9px;">IA</span>
{{ c.aiDigest }}
</div>
</div>
</div>
<div v-if="!filteredComms.length" class="rnd-table-empty" style="padding: 32px; text-align: center;">No communications found.</div>
</div>
<!-- Quick compose -->
<div class="rnd-compose">
<div class="rnd-compose-type">
<button v-for="ct in (['note', 'email', 'call'] as const)" :key="ct" class="rnd-compose-btn" :class="composeType === ct ? 'rnd-compose-btn-on' : ''" @click="composeType = ct">
<UIcon :name="commIcon(ct)" class="w-3.5 h-3.5" />
{{ ct }}
</button>
</div>
<textarea v-model="composeText" class="rnd-compose-input" placeholder="Add a note, log a call, or compose an email…" rows="2" />
<button class="rnd-compose-send" @click="composeText = ''; toast.add({ title: `${composeType} added`, color: 'green' })">
<UIcon name="i-heroicons-paper-airplane" class="w-3.5 h-3.5" />
Send
</button>
</div>
</template>
<!-- DOCUMENTS TAB -->
<template v-if="activeTab === 'documents'">
<div class="rnd-card">
<h3 class="rnd-card-title">Required Documents</h3>
<table class="rnd-table">
<thead>
<tr><th style="width: 28px;"></th><th>Document</th><th>Category</th><th>Uploaded By</th><th>Date</th></tr>
</thead>
<tbody>
<tr v-for="d in renewal.documents" :key="d.id">
<td>
<span v-if="d.fulfilled" style="color: #01696f; font-weight: 700; font-size: 16px;"></span>
<span v-else style="color: rgba(0,0,0,0.15); font-size: 20px;"></span>
</td>
<td class="font-medium">{{ d.name }}</td>
<td class="text-[var(--text-muted)]">{{ d.category.replace(/_/g, ' ') }}</td>
<td>{{ d.uploadedBy || '—' }}</td>
<td>{{ d.uploadedAt ? fmtDate(d.uploadedAt) : '—' }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<!-- COMPARISON TAB -->
<template v-if="activeTab === 'comparison'">
<template v-if="renewal.comparison">
<!-- Premium Summary -->
<div class="rnd-comp-summary">
<div class="rnd-comp-item">
<span class="rnd-comp-label">Current Premium</span>
<span class="rnd-comp-value">{{ fmtMoney(renewal.comparison.currentPremium) }}</span>
</div>
<div class="rnd-comp-arrow"></div>
<div class="rnd-comp-item">
<span class="rnd-comp-label">Renewal Premium</span>
<span class="rnd-comp-value">
{{ renewal.comparison.renewalPremium !== null ? fmtMoney(renewal.comparison.renewalPremium) : '—' }}
<span v-if="renewal.comparison.premiumDelta !== null" class="rnd-quick-delta" :class="renewal.comparison.premiumDelta > 0 ? 'rnd-delta-up' : 'rnd-delta-down'">
{{ renewal.comparison.premiumDelta > 0 ? '+' : '' }}{{ renewal.comparison.premiumDelta }}%
</span>
</span>
</div>
<div class="rnd-comp-sep" />
<div class="rnd-comp-item">
<span class="rnd-comp-label">Current Deductible</span>
<span class="rnd-comp-value">{{ fmtMoney(renewal.comparison.currentDeductible) }}</span>
</div>
<div class="rnd-comp-arrow"></div>
<div class="rnd-comp-item">
<span class="rnd-comp-label">Renewal Deductible</span>
<span class="rnd-comp-value">
{{ renewal.comparison.renewalDeductible !== null ? fmtMoney(renewal.comparison.renewalDeductible) : '—' }}
<span v-if="renewal.comparison.deductibleDelta !== null" class="rnd-quick-delta" :class="renewal.comparison.deductibleDelta > 0 ? 'rnd-delta-up' : 'rnd-delta-down'">
{{ renewal.comparison.deductibleDelta > 0 ? '+' : '' }}{{ renewal.comparison.deductibleDelta }}%
</span>
</span>
</div>
</div>
<!-- Coverage Lines -->
<div class="rnd-card">
<h3 class="rnd-card-title">Coverage Comparison</h3>
<table class="rnd-table">
<thead>
<tr><th>Coverage</th><th class="text-right">Current</th><th class="text-right">Renewal</th><th class="text-center">Delta</th></tr>
</thead>
<tbody>
<tr v-for="line in renewal.comparison.coverageLines" :key="line.name">
<td class="font-medium">{{ line.name }}</td>
<td class="text-right">{{ typeof line.currentAmount === 'number' ? fmtMoney(line.currentAmount) : line.currentAmount }}</td>
<td class="text-right">{{ line.renewalAmount !== null ? (typeof line.renewalAmount === 'number' ? fmtMoney(line.renewalAmount) : line.renewalAmount) : '—' }}</td>
<td class="text-center">
<span v-if="line.delta" :class="deltaFlagClass(line.flag)">{{ line.delta }}</span>
<span v-else class="text-[var(--text-muted)] opacity-40"></span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- AI Analysis -->
<div v-if="renewal.comparison.aiAnalysis" class="rnd-ai-panel">
<div class="rnd-ai-header">
<span class="rnd-ai-badge">IA</span>
<span class="rnd-ai-title">Coverage Analysis</span>
</div>
<div class="rnd-ai-body">
<p>{{ renewal.comparison.aiAnalysis }}</p>
</div>
</div>
</template>
<div v-else class="rnd-card" style="text-align: center; padding: 48px 20px; color: #8a8a86;">
<UIcon name="i-heroicons-clock" class="w-8 h-8" style="margin: 0 auto 12px; display: block; opacity: 0.4;" />
<p class="font-semibold" style="color: #1a1a1a;">Waiting for renewal terms</p>
<p style="font-size: 13px; margin-top: 4px;">Comparison will be available once the carrier sends renewal terms.</p>
</div>
</template>
</template>
</div>
</template>
<style scoped>
/* =====================================================================
RENEWAL DETAIL PAGE — scoped, rnd- prefix
===================================================================== */
.rnd-root {
--rnd-brand: #01696f;
--rnd-brand-soft: rgba(1, 105, 111, 0.06);
--rnd-border: rgba(0, 0, 0, 0.06);
--rnd-muted: #8a8a86;
--rnd-red: #c13838;
--rnd-amber: #c27b1a;
}
/* ---- Not Found ---- */
.rnd-not-found { text-align: center; padding: 80px 0; color: var(--rnd-muted); }
.rnd-not-found h2 { font-size: 18px; font-weight: 600; margin: 12px 0 4px; color: #1a1a1a; }
.rnd-not-found p { margin-bottom: 16px; font-size: 13px; }
/* ---- Back link ---- */
.rnd-back-link {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 500; color: var(--rnd-muted);
text-decoration: none; margin-bottom: 12px; transition: color 150ms ease;
}
.rnd-back-link:hover { color: var(--rnd-brand); }
/* ---- Header ---- */
.rnd-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.rnd-header-left { display: flex; flex-direction: column; gap: 6px; }
.rnd-header-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rnd-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.rnd-title { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; color: #1a1a1a; line-height: 1; font-family: 'SF Mono', 'Fira Code', monospace; }
.rnd-subtitle { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #5c5650; flex-wrap: wrap; }
.rnd-subtitle-link { color: var(--rnd-brand); text-decoration: none; font-weight: 500; }
.rnd-subtitle-link:hover { text-decoration: underline; }
.rnd-sep { color: var(--rnd-muted); opacity: 0.5; }
.rnd-meta-row { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--rnd-muted); flex-wrap: wrap; }
/* ---- Action button ---- */
.rnd-action-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 16px; border-radius: 8px;
background: #01696f; color: #fff;
font-size: 13px; font-weight: 500; border: none;
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
}
.rnd-action-btn:hover { background: #015458; }
/* ---- Status pills ---- */
.rnd-carrier-pill { display: inline-flex; padding: 2px 8px; border-radius: 8px; font-size: 11px; font-weight: 600; white-space: nowrap; }
.rnd-csp-pending { background: rgba(138,138,134,0.08); color: #8a8a86; }
.rnd-csp-terms { background: rgba(59,130,246,0.08); color: #2563eb; }
.rnd-csp-remarket { background: rgba(147,51,234,0.08); color: #9333ea; }
.rnd-csp-bound { background: rgba(22,163,74,0.08); color: #16a34a; }
.rnd-csp-declined { background: rgba(193,56,56,0.08); color: #c13838; }
.rnd-csp-lapsed { background: rgba(0,0,0,0.06); color: #6b6b68; }
.rnd-workflow-pill { display: inline-flex; padding: 2px 7px; border-radius: 6px; font-size: 10px; font-weight: 600; white-space: nowrap; border: 1px solid rgba(1,105,111,0.3); color: #01696f; }
/* ---- Priority / Risk badges ---- */
.rnd-priority-badge, .rnd-risk-badge { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; white-space: nowrap; }
.rnd-pri-critical { background: rgba(193,56,56,0.12); color: #c13838; }
.rnd-pri-high { background: rgba(194,123,26,0.08); color: #c27b1a; }
.rnd-pri-medium { background: rgba(0,0,0,0.05); color: #6b6b68; }
.rnd-pri-low { background: rgba(0,0,0,0.03); color: #8a8a86; }
.rnd-risk-high { background: rgba(193,56,56,0.08); color: #c13838; }
.rnd-risk-medium { background: rgba(194,123,26,0.08); color: #c27b1a; }
.rnd-risk-low { background: rgba(22,163,74,0.08); color: #16a34a; }
/* ---- Days ---- */
.rnd-days-past { color: #8a8a86; }
.rnd-days-critical { color: #c13838; font-weight: 600; }
.rnd-days-warn { color: #c27b1a; }
/* ---- Party strip ---- */
.rnd-party-strip { display: flex; gap: 8px; overflow-x: auto; padding: 4px 0; margin: 8px 0; }
.rnd-party-chip { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 10px; border: 1px solid var(--rnd-border); background: #fff; cursor: pointer; transition: all 150ms ease; white-space: nowrap; position: relative; }
.rnd-party-chip:hover { border-color: rgba(1,105,111,0.3); }
.rnd-party-active { border-color: #01696f; background: var(--rnd-brand-soft); }
.rnd-party-avatar { width: 24px; height: 24px; border-radius: 50%; background: rgba(1,105,111,0.08); color: #01696f; font-size: 9px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.rnd-party-info { display: flex; flex-direction: column; }
.rnd-party-role { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--rnd-muted); }
.rnd-party-name { font-size: 12px; font-weight: 600; color: #1a1a1a; }
.rnd-party-dot { width: 7px; height: 7px; border-radius: 50%; background: #c13838; position: absolute; top: 6px; right: 6px; }
.rnd-party-add { border-style: dashed; color: var(--rnd-muted); font-size: 12px; font-weight: 500; }
.rnd-party-add:hover { color: #01696f; border-color: rgba(1,105,111,0.3); }
/* ---- Quick stats ---- */
.rnd-quick-strip { display: flex; align-items: center; gap: 16px; padding: 14px 18px; border-radius: 12px; border: 1px solid var(--rnd-border); background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow-x: auto; }
.rnd-quick-item { display: flex; align-items: center; gap: 8px; }
.rnd-quick-icon { color: var(--rnd-muted); flex-shrink: 0; }
.rnd-quick-data { display: flex; flex-direction: column; }
.rnd-quick-value { font-size: 15px; font-weight: 700; color: #1a1a1a; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rnd-quick-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--rnd-muted); }
.rnd-quick-sep { width: 1px; height: 28px; background: var(--rnd-border); flex-shrink: 0; }
.rnd-quick-delta { font-size: 11px; font-weight: 700; margin-left: 4px; }
.rnd-delta-up { color: #c13838; }
.rnd-delta-down { color: #16a34a; }
.rnd-delta-new { color: #2563eb; font-weight: 600; }
.rnd-delta-removed { color: #c13838; text-decoration: line-through; }
/* ---- Action panel ---- */
.rnd-action-panel { border-radius: 12px; border: 1px solid var(--rnd-border); background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow: hidden; }
.rnd-action-header { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 14px; font-weight: 700; color: #1a1a1a; border-bottom: 1px solid var(--rnd-border); }
.rnd-action-count { font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 9999px; background: rgba(193,56,56,0.08); color: #c13838; }
.rnd-action-row { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid rgba(0,0,0,0.03); font-size: 13px; }
.rnd-action-row:last-child { border-bottom: none; }
.rnd-action-title { flex: 1; font-weight: 500; }
.rnd-action-assignee { font-size: 12px; color: var(--rnd-muted); min-width: 60px; }
.rnd-action-due { font-size: 12px; color: var(--rnd-muted); min-width: 80px; }
.rnd-action-overdue { color: #c13838 !important; font-weight: 600; }
.rnd-action-btns { display: flex; gap: 4px; }
.rnd-btn-escalate { padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; background: rgba(193,56,56,0.06); color: #c13838; border: 1px solid rgba(193,56,56,0.15); cursor: pointer; }
.rnd-btn-dismiss { padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; background: transparent; color: var(--rnd-muted); border: 1px solid var(--rnd-border); cursor: pointer; }
.rnd-btn-draft { padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; background: var(--rnd-brand-soft); color: #01696f; border: 1px solid rgba(1,105,111,0.15); cursor: pointer; }
/* ---- SLA dot ---- */
.rnd-sla-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.rnd-sla-green { background: #059669; }
.rnd-sla-amber { background: #c27b1a; }
.rnd-sla-red { background: #c13838; }
/* ---- AI panel ---- */
.rnd-ai-panel { background: var(--rnd-brand-soft); border: 1px solid var(--rnd-border); border-left: 3px solid #01696f; border-radius: 12px; overflow: hidden; }
.rnd-ai-header { display: flex; align-items: center; gap: 10px; padding: 14px 18px; cursor: pointer; }
.rnd-ai-badge { padding: 2px 7px; border-radius: 6px; font-size: 10px; font-weight: 700; background: #01696f; color: white; }
.rnd-ai-title { font-size: 14px; font-weight: 700; color: #1a1a1a; flex: 1; }
.rnd-ai-toggle { color: var(--rnd-muted); }
.rnd-ai-body { padding: 0 18px 16px; font-size: 13px; line-height: 1.6; color: #3a3a38; }
.rnd-ai-section { margin-top: 14px; }
.rnd-ai-section-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: #01696f; margin-bottom: 6px; }
.rnd-ai-list { padding-left: 18px; font-size: 13px; line-height: 1.7; }
.rnd-ai-list li { margin-bottom: 4px; }
.rnd-ai-regen { display: inline-flex; align-items: center; gap: 4px; margin-top: 12px; padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; color: #01696f; background: rgba(1,105,111,0.08); border: none; cursor: pointer; }
.rnd-ai-regen:hover { background: rgba(1,105,111,0.15); }
/* ---- Tabs ---- */
.rnd-main-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--rnd-border); margin-top: 8px; }
.rnd-main-tab { display: flex; align-items: center; gap: 6px; padding: 10px 16px; font-size: 13px; font-weight: 500; color: var(--rnd-muted); border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; transition: all 150ms ease; }
.rnd-main-tab-on { color: #01696f; border-bottom-color: #01696f; font-weight: 600; }
.rnd-main-tab-off:hover { color: #1a1a1a; }
/* ---- Card ---- */
.rnd-card { background: #fff; border: 1px solid var(--rnd-border); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.03); margin-top: 16px; }
.rnd-card-title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 12px; }
.rnd-card-cancel { border-left: 3px solid #c13838; }
/* ---- Table ---- */
.rnd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.rnd-table thead th { padding: 8px 12px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #8a8a86; border-bottom: 1px solid var(--rnd-border); text-align: left; white-space: nowrap; }
.rnd-table tbody td { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); vertical-align: middle; }
.rnd-table tbody tr:last-child td { border-bottom: none; }
.rnd-table-empty { text-align: center; padding: 24px !important; color: var(--rnd-muted); font-size: 13px; }
/* ---- Task status ---- */
.rnd-ts-open { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(59,130,246,0.08); color: #2563eb; }
.rnd-ts-progress { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(1,105,111,0.08); color: #01696f; }
.rnd-ts-overdue { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(193,56,56,0.08); color: #c13838; }
.rnd-ts-done { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(0,0,0,0.04); color: #8a8a86; }
/* ---- History outcomes ---- */
.rnd-outcome { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; white-space: nowrap; }
.rnd-outcome-renewed { background: rgba(22,163,74,0.08); color: #16a34a; }
.rnd-outcome-remarketed { background: rgba(147,51,234,0.08); color: #9333ea; }
.rnd-outcome-cancelled { background: rgba(193,56,56,0.08); color: #c13838; }
.rnd-outcome-new { background: rgba(59,130,246,0.08); color: #2563eb; }
/* ---- History bar ---- */
.rnd-bar-wrap { height: 6px; border-radius: 3px; background: rgba(0,0,0,0.04); overflow: hidden; }
.rnd-bar { height: 100%; border-radius: 3px; background: #01696f; transition: width 300ms ease; }
/* ---- Parties grid ---- */
.rnd-parties-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.rnd-party-card { display: flex; gap: 12px; padding: 14px; border-radius: 10px; border: 1px solid var(--rnd-border); }
.rnd-party-card-avatar { width: 36px; height: 36px; border-radius: 50%; background: rgba(1,105,111,0.08); color: #01696f; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.rnd-party-card-name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.rnd-party-card-role { font-size: 11px; text-transform: uppercase; color: var(--rnd-muted); letter-spacing: 0.03em; }
.rnd-party-card-contact { font-size: 12px; color: #5c5650; margin-top: 2px; }
/* ---- Cancellation grid ---- */
.rnd-cancel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; }
.rnd-cancel-label { display: block; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--rnd-muted); margin-bottom: 2px; }
.rnd-cancel-value { font-size: 14px; font-weight: 600; color: #1a1a1a; }
/* ---- Communications ---- */
.rnd-comm-filters { display: flex; gap: 6px; margin-top: 12px; }
.rnd-comm-chip { display: inline-flex; align-items: center; gap: 4px; padding: 5px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; border: 1px solid var(--rnd-border); background: #fff; cursor: pointer; transition: all 150ms ease; text-transform: capitalize; }
.rnd-comm-chip-on { background: #01696f; color: white; border-color: #01696f; }
.rnd-comm-chip-off { color: var(--rnd-muted); }
.rnd-comm-chip-off:hover { color: #1a1a1a; border-color: rgba(0,0,0,0.15); }
.rnd-timeline { margin-top: 16px; display: flex; flex-direction: column; gap: 0; }
.rnd-timeline-item { display: flex; gap: 14px; padding: 16px 0; border-bottom: 1px solid rgba(0,0,0,0.04); }
.rnd-timeline-item:last-child { border-bottom: none; }
.rnd-timeline-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.rnd-ti-email { background: rgba(59,130,246,0.08); color: #2563eb; }
.rnd-ti-call { background: rgba(22,163,74,0.08); color: #16a34a; }
.rnd-ti-note { background: rgba(194,123,26,0.08); color: #c27b1a; }
.rnd-ti-system { background: rgba(1,105,111,0.08); color: #01696f; }
.rnd-timeline-content { flex: 1; min-width: 0; }
.rnd-timeline-head { display: flex; align-items: center; gap: 6px; font-size: 12px; flex-wrap: wrap; }
.rnd-timeline-from { font-weight: 600; color: #1a1a1a; }
.rnd-timeline-to { color: var(--rnd-muted); }
.rnd-timeline-dir { font-size: 10px; padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.04); color: var(--rnd-muted); }
.rnd-timeline-tpl { font-size: 9px; padding: 1px 5px; border-radius: 4px; background: rgba(1,105,111,0.06); color: #01696f; font-weight: 600; }
.rnd-timeline-time { font-size: 11px; color: var(--rnd-muted); margin-left: auto; }
.rnd-timeline-subject { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-top: 4px; }
.rnd-timeline-body { font-size: 13px; line-height: 1.5; color: #5c5650; margin-top: 4px; white-space: pre-line; }
.rnd-ai-digest { margin-top: 8px; padding: 8px 12px; border-radius: 8px; background: var(--rnd-brand-soft); border-left: 2px solid #01696f; font-size: 12px; color: #3a3a38; display: flex; align-items: flex-start; gap: 8px; }
/* ---- Compose ---- */
.rnd-compose { margin-top: 16px; border: 1px solid var(--rnd-border); border-radius: 12px; background: #fff; padding: 14px; }
.rnd-compose-type { display: flex; gap: 4px; margin-bottom: 10px; }
.rnd-compose-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; border: 1px solid var(--rnd-border); background: #fff; color: var(--rnd-muted); cursor: pointer; text-transform: capitalize; }
.rnd-compose-btn-on { background: #01696f; color: white; border-color: #01696f; }
.rnd-compose-input { width: 100%; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 10px 12px; font-size: 13px; resize: vertical; }
.rnd-compose-input:focus { outline: none; border-color: #01696f; }
.rnd-compose-send { margin-top: 8px; display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; border-radius: 8px; font-size: 12px; font-weight: 600; background: #01696f; color: white; border: none; cursor: pointer; }
.rnd-compose-send:hover { background: #015458; }
/* ---- Comparison summary ---- */
.rnd-comp-summary { display: flex; align-items: center; gap: 16px; padding: 20px; border-radius: 12px; border: 1px solid var(--rnd-border); background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03); flex-wrap: wrap; margin-top: 16px; }
.rnd-comp-item { display: flex; flex-direction: column; }
.rnd-comp-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--rnd-muted); }
.rnd-comp-value { font-size: 18px; font-weight: 700; color: #1a1a1a; font-variant-numeric: tabular-nums; }
.rnd-comp-arrow { font-size: 18px; color: var(--rnd-muted); }
.rnd-comp-sep { width: 1px; height: 36px; background: var(--rnd-border); }
</style>