WIP jordan
This commit is contained in:
499
app/pages/renewals/index.vue
Normal file
499
app/pages/renewals/index.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user