500 lines
22 KiB
Vue
500 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
MOCK_RENEWALS, carrierStatusLabels, brokerStatusLabels, priorityLabels,
|
|
retentionRiskLabels, expiryBuckets, slaColor,
|
|
type Renewal, type CarrierRenewalStatus, type BrokerRenewalStatus,
|
|
type RenewalPriority, type RetentionRisk,
|
|
} from '~/data/mock-renewals'
|
|
|
|
usePageTitle('Renewals')
|
|
|
|
const renewals = ref<Renewal[]>([...MOCK_RENEWALS])
|
|
|
|
// ── View toggle ─────────────────────────────────────────────────────────────
|
|
const viewMode = ref<'my' | 'all'>('all')
|
|
|
|
// ── Filter tabs ─────────────────────────────────────────────────────────────
|
|
type PipelineFilter = 'all' | 'needs_action' | 'in_progress' | 'resolved'
|
|
const activeFilter = ref<PipelineFilter>('all')
|
|
|
|
// ── Dropdown filters ────────────────────────────────────────────────────────
|
|
const carrierFilter = ref('')
|
|
const lobFilter = ref('')
|
|
const handlerFilter = ref('')
|
|
const expiryFilter = ref('')
|
|
const priorityFilter = ref('')
|
|
const riskFilter = ref('')
|
|
const brokerStatusFilter = ref('')
|
|
|
|
const uniqueCarriers = computed(() => [...new Set(renewals.value.map(r => r.carrier))].sort())
|
|
const uniqueLobs = computed(() => [...new Set(renewals.value.map(r => r.lob))].sort())
|
|
const uniqueHandlers = computed(() => [...new Set(renewals.value.map(r => r.assignedTo))].sort())
|
|
|
|
// ── Filtering logic ─────────────────────────────────────────────────────────
|
|
const closedStatuses: BrokerRenewalStatus[] = ['closed_renewed', 'closed_remarketed', 'closed_cancelled', 'not_renewing']
|
|
const actionStatuses: BrokerRenewalStatus[] = ['unreviewed', 'under_review']
|
|
const progressStatuses: BrokerRenewalStatus[] = ['proposal_sent', 'awaiting_client_response', 'awaiting_payment']
|
|
|
|
const filteredRenewals = computed(() => {
|
|
let result = [...renewals.value]
|
|
|
|
// Tab filter
|
|
if (activeFilter.value === 'needs_action') result = result.filter(r => actionStatuses.includes(r.brokerStatus))
|
|
if (activeFilter.value === 'in_progress') result = result.filter(r => progressStatuses.includes(r.brokerStatus))
|
|
if (activeFilter.value === 'resolved') result = result.filter(r => closedStatuses.includes(r.brokerStatus))
|
|
|
|
// Dropdown filters
|
|
if (carrierFilter.value) result = result.filter(r => r.carrier === carrierFilter.value)
|
|
if (lobFilter.value) result = result.filter(r => r.lob === lobFilter.value)
|
|
if (handlerFilter.value) result = result.filter(r => r.assignedTo === handlerFilter.value)
|
|
if (priorityFilter.value) result = result.filter(r => r.priority === priorityFilter.value)
|
|
if (riskFilter.value) result = result.filter(r => r.retentionRisk === riskFilter.value)
|
|
if (brokerStatusFilter.value) result = result.filter(r => r.brokerStatus === brokerStatusFilter.value)
|
|
|
|
if (expiryFilter.value) {
|
|
const bucket = expiryBuckets[expiryFilter.value as keyof typeof expiryBuckets]
|
|
if (bucket) result = result.filter(r => r.daysUntilExpiry >= bucket.min && r.daysUntilExpiry <= bucket.max)
|
|
}
|
|
|
|
// Sort: breached first, then by days until expiry ascending
|
|
result.sort((a, b) => {
|
|
const aBreached = a.slaPercent >= 100 ? 0 : 1
|
|
const bBreached = b.slaPercent >= 100 ? 0 : 1
|
|
if (aBreached !== bBreached) return aBreached - bBreached
|
|
return a.daysUntilExpiry - b.daysUntilExpiry
|
|
})
|
|
|
|
return result
|
|
})
|
|
|
|
// ── Filter counts ───────────────────────────────────────────────────────────
|
|
const filterCounts = computed(() => ({
|
|
all: renewals.value.length,
|
|
needs_action: renewals.value.filter(r => actionStatuses.includes(r.brokerStatus)).length,
|
|
in_progress: renewals.value.filter(r => progressStatuses.includes(r.brokerStatus)).length,
|
|
resolved: renewals.value.filter(r => closedStatuses.includes(r.brokerStatus)).length,
|
|
}))
|
|
|
|
// ── KPIs ────────────────────────────────────────────────────────────────────
|
|
const kpis = computed(() => {
|
|
const active = renewals.value.filter(r => !closedStatuses.includes(r.brokerStatus))
|
|
const urgent = active.filter(r => r.daysUntilExpiry <= 7).length
|
|
const pipeline = active.reduce((s, r) => s + r.currentPremium, 0)
|
|
const atRisk = active.filter(r => r.retentionRisk === 'high').length
|
|
const breached = active.filter(r => r.slaPercent >= 100).length
|
|
const renewed = renewals.value.filter(r => r.brokerStatus === 'closed_renewed' || r.brokerStatus === 'closed_remarketed').length
|
|
return { urgent, pipeline, atRisk, breached, renewed }
|
|
})
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
function formatCurrency(n: number) {
|
|
return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 0 })
|
|
}
|
|
|
|
function daysDisplay(d: number) {
|
|
if (d < 0) return `${Math.abs(d)}d past`
|
|
if (d === 0) return 'Today'
|
|
return `${d}d`
|
|
}
|
|
|
|
function daysClass(d: number) {
|
|
if (d < 0) return 'rn-days-past'
|
|
if (d <= 7) return 'rn-days-critical'
|
|
if (d <= 30) return 'rn-days-warn'
|
|
return ''
|
|
}
|
|
|
|
const carrierPillClass = (s: CarrierRenewalStatus) => {
|
|
const map: Record<string, string> = {
|
|
pending: 'rn-csp-pending', terms_received: 'rn-csp-terms', remarketing: 'rn-csp-remarket',
|
|
bound: 'rn-csp-bound', declined: 'rn-csp-declined', lapsed: 'rn-csp-lapsed',
|
|
}
|
|
return map[s] ?? ''
|
|
}
|
|
|
|
const priorityClass = (p: RenewalPriority) => {
|
|
const map: Record<string, string> = { critical: 'rn-pri-critical', high: 'rn-pri-high', medium: 'rn-pri-medium', low: 'rn-pri-low' }
|
|
return map[p] ?? ''
|
|
}
|
|
|
|
const riskClass = (r: RetentionRisk) => {
|
|
const map: Record<string, string> = { high: 'rn-risk-high', medium: 'rn-risk-medium', low: 'rn-risk-low' }
|
|
return map[r] ?? ''
|
|
}
|
|
|
|
function clearFilters() {
|
|
carrierFilter.value = ''
|
|
lobFilter.value = ''
|
|
handlerFilter.value = ''
|
|
expiryFilter.value = ''
|
|
priorityFilter.value = ''
|
|
riskFilter.value = ''
|
|
brokerStatusFilter.value = ''
|
|
}
|
|
|
|
const hasActiveFilters = computed(() => !!(carrierFilter.value || lobFilter.value || handlerFilter.value || expiryFilter.value || priorityFilter.value || riskFilter.value || brokerStatusFilter.value))
|
|
|
|
const toast = useToast()
|
|
function handleSendReminders() {
|
|
toast.add({ title: 'Sending renewal reminders…', description: 'Batch reminder emails queued for processing.', color: 'green' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rn-page">
|
|
<!-- Header -->
|
|
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
<div class="max-w-xl">
|
|
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Renewals</h1>
|
|
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
|
Pipeline de renovaciones — seguimiento desde revisión hasta confirmación.
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<NuxtLink to="/claims/settings" class="rn-action-btn-outline">
|
|
<UIcon name="i-heroicons-cog-6-tooth" style="width: 14px; height: 14px;" />
|
|
Settings
|
|
</NuxtLink>
|
|
<button type="button" class="rn-action-btn-primary" @click="handleSendReminders">
|
|
<UIcon name="i-heroicons-paper-airplane" style="width: 14px; height: 14px;" />
|
|
Send Reminders
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KPI strip -->
|
|
<div class="rn-kpi-strip">
|
|
<div class="rn-kpi">
|
|
<p class="rn-kpi-label">Urgent (≤ 7d)</p>
|
|
<p class="rn-kpi-value" :style="kpis.urgent > 0 ? 'color: #c13838;' : ''">{{ kpis.urgent }}</p>
|
|
</div>
|
|
<div class="rn-kpi">
|
|
<p class="rn-kpi-label">Pipeline premium</p>
|
|
<p class="rn-kpi-value">{{ formatCurrency(kpis.pipeline) }}</p>
|
|
</div>
|
|
<div class="rn-kpi">
|
|
<p class="rn-kpi-label">High retention risk</p>
|
|
<p class="rn-kpi-value" :style="kpis.atRisk > 0 ? 'color: #c27b1a;' : ''">{{ kpis.atRisk }}</p>
|
|
</div>
|
|
<div class="rn-kpi">
|
|
<p class="rn-kpi-label">SLA breached</p>
|
|
<p class="rn-kpi-value" :style="kpis.breached > 0 ? 'color: #c13838;' : ''">{{ kpis.breached }}</p>
|
|
</div>
|
|
<div class="rn-kpi">
|
|
<p class="rn-kpi-label">Renewed MTD</p>
|
|
<p class="rn-kpi-value" style="color: #01696f;">{{ kpis.renewed }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View toggle + Filter tabs -->
|
|
<div class="rn-controls-row">
|
|
<div class="rn-view-toggle">
|
|
<button type="button" class="rn-view-btn" :class="viewMode === 'my' ? 'rn-view-on' : 'rn-view-off'" @click="viewMode = 'my'">My Renewals</button>
|
|
<button type="button" class="rn-view-btn" :class="viewMode === 'all' ? 'rn-view-on' : 'rn-view-off'" @click="viewMode = 'all'">All Renewals</button>
|
|
</div>
|
|
|
|
<div class="rn-filter-tabs">
|
|
<button
|
|
v-for="f in ([
|
|
{ id: 'all', label: 'All' },
|
|
{ id: 'needs_action', label: 'Needs Action' },
|
|
{ id: 'in_progress', label: 'In Progress' },
|
|
{ id: 'resolved', label: 'Resolved' },
|
|
] as { id: PipelineFilter; label: string }[])"
|
|
:key="f.id"
|
|
type="button"
|
|
class="rn-filter-tab"
|
|
:class="activeFilter === f.id ? 'rn-filter-on' : 'rn-filter-off'"
|
|
@click="activeFilter = f.id"
|
|
>
|
|
{{ f.label }}
|
|
<span class="rn-filter-count" :class="activeFilter === f.id ? 'rn-filter-count-on' : ''">{{ filterCounts[f.id] }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<span class="text-[11px] text-[var(--text-muted)] ml-auto">{{ filteredRenewals.length }} results</span>
|
|
</div>
|
|
|
|
<!-- Filter dropdowns -->
|
|
<div class="rn-dropdown-row">
|
|
<select v-model="brokerStatusFilter" class="rn-dropdown">
|
|
<option value="">Workflow</option>
|
|
<option v-for="(label, key) in brokerStatusLabels" :key="key" :value="key">{{ label }}</option>
|
|
</select>
|
|
<select v-model="carrierFilter" class="rn-dropdown">
|
|
<option value="">Carrier</option>
|
|
<option v-for="c in uniqueCarriers" :key="c" :value="c">{{ c }}</option>
|
|
</select>
|
|
<select v-model="lobFilter" class="rn-dropdown">
|
|
<option value="">LOB</option>
|
|
<option v-for="l in uniqueLobs" :key="l" :value="l">{{ l }}</option>
|
|
</select>
|
|
<select v-model="handlerFilter" class="rn-dropdown">
|
|
<option value="">Handler</option>
|
|
<option v-for="h in uniqueHandlers" :key="h" :value="h">{{ h }}</option>
|
|
</select>
|
|
<select v-model="expiryFilter" class="rn-dropdown">
|
|
<option value="">Expiry</option>
|
|
<option v-for="(b, key) in expiryBuckets" :key="key" :value="key">{{ b.label }}</option>
|
|
</select>
|
|
<select v-model="priorityFilter" class="rn-dropdown">
|
|
<option value="">Priority</option>
|
|
<option v-for="(label, key) in priorityLabels" :key="key" :value="key">{{ label }}</option>
|
|
</select>
|
|
<select v-model="riskFilter" class="rn-dropdown">
|
|
<option value="">Risk</option>
|
|
<option v-for="(label, key) in retentionRiskLabels" :key="key" :value="key">{{ label }}</option>
|
|
</select>
|
|
<button v-if="hasActiveFilters" class="rn-clear-btn" @click="clearFilters">
|
|
<UIcon name="i-heroicons-x-mark" class="w-3 h-3" />
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="rn-table-wrap">
|
|
<table class="rn-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 28px;"></th>
|
|
<th>Renewal</th>
|
|
<th>Customer</th>
|
|
<th>LOB / Carrier</th>
|
|
<th>Status</th>
|
|
<th class="text-right">Current</th>
|
|
<th class="text-right">Renewal</th>
|
|
<th class="text-right">Expiry</th>
|
|
<th>Risk</th>
|
|
<th>Priority</th>
|
|
<th>Handler</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="r in filteredRenewals"
|
|
:key="r.id"
|
|
class="rn-row"
|
|
:class="{ 'rn-breach-row': r.slaPercent >= 100 && !closedStatuses.includes(r.brokerStatus) }"
|
|
style="cursor: pointer;"
|
|
@click="navigateTo(`/renewals/${r.id}`)"
|
|
>
|
|
<td><span class="rn-sla-dot" :class="`rn-sla-${slaColor(r.slaPercent)}`" /></td>
|
|
<td>
|
|
<NuxtLink :to="`/renewals/${r.id}`" class="rn-id-link" @click.stop>{{ r.id }}</NuxtLink>
|
|
<p class="text-[10px] text-[var(--text-muted)] font-mono mt-0.5">{{ r.policyNumber }}</p>
|
|
</td>
|
|
<td>
|
|
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ r.customerName || 'Unnamed customer' }}</p>
|
|
<p class="text-[10px] text-[var(--text-muted)] uppercase">{{ r.customerType }}</p>
|
|
</td>
|
|
<td>
|
|
<p class="text-[13px] text-[var(--text-primary)]">{{ r.lob }}</p>
|
|
<p class="text-[11px] text-[var(--text-muted)]">{{ r.carrier }}</p>
|
|
</td>
|
|
<td>
|
|
<div class="rn-dual-status">
|
|
<span class="rn-carrier-pill" :class="carrierPillClass(r.carrierStatus)">{{ carrierStatusLabels[r.carrierStatus] }}</span>
|
|
<span class="rn-workflow-pill">{{ brokerStatusLabels[r.brokerStatus] }}</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-right text-[13px] font-semibold text-[var(--text-primary)]">{{ formatCurrency(r.currentPremium) }}</td>
|
|
<td class="text-right text-[13px]">
|
|
<template v-if="r.renewalPremium !== null">
|
|
<span class="font-semibold text-[var(--text-primary)]">{{ formatCurrency(r.renewalPremium) }}</span>
|
|
<span
|
|
v-if="r.premiumDelta !== null"
|
|
class="rn-delta"
|
|
:class="r.premiumDelta > 0 ? 'rn-delta-up' : r.premiumDelta < 0 ? 'rn-delta-down' : ''"
|
|
>{{ r.premiumDelta > 0 ? '+' : '' }}{{ r.premiumDelta }}%</span>
|
|
</template>
|
|
<span v-else class="text-[var(--text-muted)] opacity-40">—</span>
|
|
</td>
|
|
<td class="text-right">
|
|
<span class="text-[13px] font-bold" :class="daysClass(r.daysUntilExpiry)">{{ daysDisplay(r.daysUntilExpiry) }}</span>
|
|
<p class="text-[10px] text-[var(--text-muted)]">{{ r.expiryDate }}</p>
|
|
</td>
|
|
<td>
|
|
<span class="rn-risk-pill" :class="riskClass(r.retentionRisk)">{{ retentionRiskLabels[r.retentionRisk] }}</span>
|
|
</td>
|
|
<td>
|
|
<span :class="priorityClass(r.priority)">{{ priorityLabels[r.priority] }}</span>
|
|
</td>
|
|
<td class="text-[13px] text-[var(--text-muted)]">
|
|
<span :class="r.assignedTo === 'Unassigned' ? 'rn-unassigned' : ''">{{ r.assignedTo }}</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rn-page {
|
|
max-width: 76rem; margin: 0 auto;
|
|
display: flex; flex-direction: column; gap: 20px; padding-bottom: 3rem;
|
|
}
|
|
|
|
/* ── Action buttons ── */
|
|
.rn-action-btn-primary {
|
|
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;
|
|
}
|
|
.rn-action-btn-primary:hover { background: #015458; }
|
|
.rn-action-btn-outline {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 8px 16px; border-radius: 8px;
|
|
background: transparent; color: var(--text-secondary);
|
|
font-size: 13px; font-weight: 500;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
|
|
text-decoration: none;
|
|
}
|
|
.rn-action-btn-outline:hover { border-color: #01696f; color: #01696f; }
|
|
|
|
/* ── KPI strip ── */
|
|
.rn-kpi-strip {
|
|
display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
|
|
background: rgba(0,0,0,0.06); box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow: hidden;
|
|
}
|
|
.rn-kpi { padding: 14px 18px; background: #fff; }
|
|
.rn-kpi:first-child { border-radius: 12px 0 0 12px; }
|
|
.rn-kpi:last-child { border-radius: 0 12px 12px 0; }
|
|
.rn-kpi-label {
|
|
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: #8a8a86;
|
|
}
|
|
.rn-kpi-value {
|
|
margin-top: 4px; font-size: 22px; font-weight: 600;
|
|
color: var(--text-primary); font-variant-numeric: tabular-nums;
|
|
}
|
|
@media (max-width: 640px) { .rn-kpi-strip { grid-template-columns: repeat(2, 1fr); } }
|
|
|
|
/* ── Controls row ── */
|
|
.rn-controls-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
|
|
/* ── View toggle ── */
|
|
.rn-view-toggle { display: inline-flex; gap: 1px; padding: 2px; border-radius: 8px; background: rgba(0,0,0,0.04); }
|
|
.rn-view-btn { padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; border: none; cursor: pointer; transition: all 150ms ease; white-space: nowrap; }
|
|
.rn-view-on { background: #01696f; color: white; }
|
|
.rn-view-off { background: transparent; color: #8a8a86; }
|
|
.rn-view-off:hover { color: var(--text-primary); }
|
|
|
|
/* ── Filter tabs ── */
|
|
.rn-filter-tabs { display: inline-flex; gap: 2px; padding: 3px; border-radius: 10px; background: rgba(0,0,0,0.04); }
|
|
.rn-filter-tab {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 6px 12px; border-radius: 8px;
|
|
font-size: 12px; font-weight: 500; border: none;
|
|
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
|
|
}
|
|
.rn-filter-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.rn-filter-off { background: transparent; color: var(--text-muted); }
|
|
.rn-filter-off:hover { color: var(--text-primary); }
|
|
.rn-filter-count { font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 9999px; background: rgba(0,0,0,0.06); color: var(--text-muted); }
|
|
.rn-filter-count-on { background: rgba(1,105,111,0.1); color: #01696f; }
|
|
|
|
/* ── Dropdown filters ── */
|
|
.rn-dropdown-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
.rn-dropdown {
|
|
padding: 5px 10px; border-radius: 8px; font-size: 12px; font-weight: 500;
|
|
border: 1px solid rgba(0,0,0,0.08); background: #fff; color: var(--text-primary);
|
|
cursor: pointer; min-width: 100px;
|
|
}
|
|
.rn-dropdown:focus { outline: none; border-color: #01696f; }
|
|
.rn-clear-btn {
|
|
display: inline-flex; align-items: center; gap: 4px; padding: 5px 10px;
|
|
border-radius: 8px; 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;
|
|
}
|
|
.rn-clear-btn:hover { background: rgba(193,56,56,0.12); }
|
|
|
|
/* ── Table ── */
|
|
.rn-table-wrap {
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
overflow-x: auto;
|
|
}
|
|
.rn-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.rn-table thead th {
|
|
padding: 10px 14px; font-size: 11px; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.04em; color: #8a8a86;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
white-space: nowrap; text-align: left;
|
|
}
|
|
.rn-table tbody td {
|
|
padding: 12px 14px; border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
vertical-align: top;
|
|
}
|
|
.rn-row { transition: background 100ms ease; }
|
|
.rn-row:hover { background: rgba(0,0,0,0.015); }
|
|
.rn-row:last-child td { border-bottom: none; }
|
|
|
|
/* ── Breach row ── */
|
|
.rn-breach-row { box-shadow: inset 3px 0 0 #c13838; }
|
|
|
|
/* ── SLA dot ── */
|
|
.rn-sla-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
.rn-sla-green { background: #059669; }
|
|
.rn-sla-amber { background: #c27b1a; }
|
|
.rn-sla-red { background: #c13838; }
|
|
|
|
/* ── ID link ── */
|
|
.rn-id-link {
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 12px; font-weight: 600; color: #01696f;
|
|
text-decoration: none;
|
|
}
|
|
.rn-id-link:hover { text-decoration: underline; }
|
|
|
|
/* ── Dual status pills ── */
|
|
.rn-dual-status { display: flex; flex-direction: column; gap: 3px; }
|
|
.rn-carrier-pill {
|
|
display: inline-flex; padding: 2px 7px; border-radius: 8px;
|
|
font-size: 10px; font-weight: 600; white-space: nowrap;
|
|
}
|
|
.rn-csp-pending { background: rgba(138,138,134,0.08); color: #8a8a86; }
|
|
.rn-csp-terms { background: rgba(59,130,246,0.08); color: #2563eb; }
|
|
.rn-csp-remarket { background: rgba(147,51,234,0.08); color: #9333ea; }
|
|
.rn-csp-bound { background: rgba(22,163,74,0.08); color: #16a34a; }
|
|
.rn-csp-declined { background: rgba(193,56,56,0.08); color: #c13838; }
|
|
.rn-csp-lapsed { background: rgba(0,0,0,0.06); color: #6b6b68; }
|
|
|
|
.rn-workflow-pill {
|
|
display: inline-flex; padding: 0; border-radius: 0;
|
|
font-size: 10px; font-weight: 500; white-space: nowrap;
|
|
border: none; color: var(--text-muted);
|
|
}
|
|
|
|
/* ── Premium delta ── */
|
|
.rn-delta { font-size: 10px; font-weight: 700; margin-left: 4px; }
|
|
.rn-delta-up { color: #c13838; }
|
|
.rn-delta-down { color: #16a34a; }
|
|
|
|
/* ── Days display ── */
|
|
.rn-days-past { color: #8a8a86; opacity: 0.6; }
|
|
.rn-days-critical { color: #c13838; }
|
|
.rn-days-warn { color: #c27b1a; }
|
|
|
|
/* ── Risk pills ── */
|
|
.rn-risk-pill { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; white-space: nowrap; }
|
|
.rn-risk-high { background: rgba(193,56,56,0.08); color: #c13838; }
|
|
.rn-risk-medium { background: rgba(194,123,26,0.08); color: #c27b1a; }
|
|
.rn-risk-low { background: rgba(22,163,74,0.08); color: #16a34a; }
|
|
|
|
/* ── Priority badges ── */
|
|
.rn-pri-critical { font-size: 10px; font-weight: 700; padding: 1px 7px; border-radius: 9999px; background: rgba(193,56,56,0.12); color: #c13838; white-space: nowrap; }
|
|
.rn-pri-high { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap; }
|
|
.rn-pri-medium { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(0,0,0,0.05); color: #6b6b68; white-space: nowrap; }
|
|
.rn-pri-low { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; background: rgba(0,0,0,0.03); color: #8a8a86; white-space: nowrap; }
|
|
|
|
/* ── Unassigned ── */
|
|
.rn-unassigned { font-style: italic; color: #c27b1a; }
|
|
</style>
|