WIP jordan
This commit is contained in:
1198
app/pages/claims/[id].vue
Normal file
1198
app/pages/claims/[id].vue
Normal file
File diff suppressed because it is too large
Load Diff
506
app/pages/claims/index.vue
Normal file
506
app/pages/claims/index.vue
Normal file
@@ -0,0 +1,506 @@
|
||||
<script setup lang="ts">
|
||||
import { slaColor, CARRIER_STATUS_LABELS, WORKFLOW_STATUS_LABELS } from '~/data/mock-claims'
|
||||
import type { CarrierStatus, BrokerWorkflowStatus } from '~/data/mock-claims'
|
||||
|
||||
usePageTitle('Claims')
|
||||
|
||||
interface Claim {
|
||||
id: string
|
||||
customer: string
|
||||
agent: string
|
||||
line: string
|
||||
type: string
|
||||
carrier: string
|
||||
reserved: string
|
||||
paid: string
|
||||
daysOpen: number
|
||||
priority: 'critical' | 'high' | 'medium' | 'low'
|
||||
status: 'open' | 'under_review' | 'awaiting_docs' | 'approved' | 'denied' | 'closed'
|
||||
docsPending: number
|
||||
opened: string
|
||||
carrierStatus: CarrierStatus
|
||||
workflowStatus: BrokerWorkflowStatus
|
||||
slaPercent: number
|
||||
handler: string
|
||||
}
|
||||
|
||||
const claims = ref<Claim[]>([
|
||||
{ id: 'CLM-0048', customer: 'Hotel Pacífico', agent: 'Marco V.', line: 'General Risk', type: 'Fire damage', carrier: 'ASSA', reserved: '$128,000', paid: '$0', daysOpen: 3, priority: 'critical', status: 'open', docsPending: 4, opened: 'Apr 2, 2026', carrierStatus: 'investigation', workflowStatus: 'waiting_carrier', slaPercent: 110, handler: 'Ana R.' },
|
||||
{ id: 'CLM-0047', customer: 'Empresa ABC S.A.', agent: 'Ana R.', line: 'Auto', type: 'Collision — fleet unit #7', carrier: 'Qualitas', reserved: '$14,200', paid: '$0', daysOpen: 5, priority: 'high', status: 'under_review', docsPending: 2, opened: 'Mar 31, 2026', carrierStatus: 'documentation_pending', workflowStatus: 'waiting_insured_docs', slaPercent: 60, handler: 'Ana R.' },
|
||||
{ id: 'CLM-0046', customer: 'Jorge Herrera', agent: 'Marco V.', line: 'Auto', type: 'Windshield replacement', carrier: 'Qualitas', reserved: '$1,100', paid: '$0', daysOpen: 8, priority: 'low', status: 'awaiting_docs', docsPending: 1, opened: 'Mar 28, 2026', carrierStatus: 'documentation_pending', workflowStatus: 'waiting_insured_docs', slaPercent: 40, handler: 'Marco V.' },
|
||||
{ id: 'CLM-0045', customer: 'Clínica San José', agent: 'Ana R.', line: 'Life', type: 'Surgery pre-auth', carrier: 'Pan-American Life', reserved: '$23,500', paid: '$0', daysOpen: 12, priority: 'high', status: 'under_review', docsPending: 0, opened: 'Mar 24, 2026', carrierStatus: 'reserved', workflowStatus: 'waiting_carrier', slaPercent: 85, handler: 'Ana R.' },
|
||||
{ id: 'CLM-0044', customer: 'Carmen Ruiz', agent: 'Ana R.', line: 'Life', type: 'Outpatient claim', carrier: 'Pan-American Life', reserved: '$3,800', paid: '$3,200', daysOpen: 18, priority: 'medium', status: 'approved', docsPending: 0, opened: 'Mar 18, 2026', carrierStatus: 'settlement_offered', workflowStatus: 'ready_to_close', slaPercent: 50, handler: 'Ana R.' },
|
||||
{ id: 'CLM-0043', customer: 'Supermercado Tico', agent: 'Marco V.', line: 'General Risk', type: 'Customer injury — store premises', carrier: 'Mapfre', reserved: '$45,000', paid: '$0', daysOpen: 22, priority: 'high', status: 'under_review', docsPending: 3, opened: 'Mar 14, 2026', carrierStatus: 'negotiation', workflowStatus: 'client_update_overdue', slaPercent: 100, handler: 'Marco V.' },
|
||||
{ id: 'CLM-0042', customer: 'Isabel Mora', agent: 'Ana R.', line: 'Auto', type: 'Theft — total loss', carrier: 'ASSA', reserved: '$18,500', paid: '$18,500', daysOpen: 35, priority: 'medium', status: 'closed', docsPending: 0, opened: 'Mar 1, 2026', carrierStatus: 'closed', workflowStatus: 'ready_to_close', slaPercent: 95, handler: 'Ana R.' },
|
||||
{ id: 'CLM-0041', customer: 'Manuel Torres', agent: 'Marco V.', line: 'Life', type: 'Disability benefit', carrier: 'Pan-American Life', reserved: '$52,000', paid: '$12,000', daysOpen: 41, priority: 'medium', status: 'approved', docsPending: 0, opened: 'Feb 23, 2026', carrierStatus: 'reserved', workflowStatus: 'waiting_carrier', slaPercent: 70, handler: 'Marco V.' },
|
||||
])
|
||||
|
||||
// ── View toggle ─────────────────────────────────────────────────────────────
|
||||
const viewMode = ref<'my' | 'all'>('all')
|
||||
|
||||
type ClaimFilter = 'all' | 'active' | 'resolved'
|
||||
const activeFilter = ref<ClaimFilter>('all')
|
||||
|
||||
// ── Filter dropdowns ────────────────────────────────────────────────────────
|
||||
const statusFilter = ref('')
|
||||
const carrierFilter = ref('')
|
||||
const lobFilter = ref('')
|
||||
const handlerFilter = ref('')
|
||||
const agingFilter = ref('')
|
||||
const priorityFilter = ref('')
|
||||
|
||||
const uniqueCarriers = computed(() => [...new Set(claims.value.map(c => c.carrier))].sort())
|
||||
const uniqueLobs = computed(() => [...new Set(claims.value.map(c => c.line))].sort())
|
||||
const uniqueHandlers = computed(() => [...new Set(claims.value.map(c => c.handler))].sort())
|
||||
|
||||
const filteredClaims = computed(() => {
|
||||
let result = [...claims.value]
|
||||
|
||||
if (activeFilter.value === 'active') result = result.filter(c => !['closed', 'denied'].includes(c.status))
|
||||
if (activeFilter.value === 'resolved') result = result.filter(c => ['closed', 'denied'].includes(c.status))
|
||||
|
||||
if (statusFilter.value) result = result.filter(c => c.status === statusFilter.value)
|
||||
if (carrierFilter.value) result = result.filter(c => c.carrier === carrierFilter.value)
|
||||
if (lobFilter.value) result = result.filter(c => c.line === lobFilter.value)
|
||||
if (handlerFilter.value) result = result.filter(c => c.handler === handlerFilter.value)
|
||||
if (priorityFilter.value) result = result.filter(c => c.priority === priorityFilter.value)
|
||||
|
||||
if (agingFilter.value) {
|
||||
const ranges: Record<string, [number, number]> = { '0-7': [0, 7], '8-14': [8, 14], '15-30': [15, 30], '30+': [30, 9999] }
|
||||
const [min, max] = ranges[agingFilter.value] ?? [0, 9999]
|
||||
result = result.filter(c => c.daysOpen >= min && c.daysOpen <= max)
|
||||
}
|
||||
|
||||
// Sort: breached first
|
||||
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 b.slaPercent - a.slaPercent
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const filterCounts = computed(() => ({
|
||||
all: claims.value.length,
|
||||
active: claims.value.filter(c => !['closed', 'denied'].includes(c.status)).length,
|
||||
resolved: claims.value.filter(c => ['closed', 'denied'].includes(c.status)).length,
|
||||
}))
|
||||
|
||||
const kpis = computed(() => {
|
||||
const active = claims.value.filter(c => !['closed', 'denied'].includes(c.status))
|
||||
const underReview = claims.value.filter(c => c.status === 'under_review').length
|
||||
const avgDays = active.length ? Math.round(active.reduce((s, c) => s + c.daysOpen, 0) / active.length) : 0
|
||||
const totalReserved = claims.value.filter(c => c.status !== 'closed').reduce((s, c) => s + parseFloat(c.reserved.replace(/[$,]/g, '')), 0)
|
||||
const breached = claims.value.filter(c => c.slaPercent >= 100 && !['closed', 'denied'].includes(c.status)).length
|
||||
return { openClaims: active.length, underReview, avgDays, totalReserved, breached }
|
||||
})
|
||||
|
||||
const statusMeta: Record<string, { label: string; class: string }> = {
|
||||
open: { label: 'Open', class: 'cl-st-open' },
|
||||
under_review: { label: 'Under Review', class: 'cl-st-review' },
|
||||
awaiting_docs: { label: 'Awaiting Docs', class: 'cl-st-docs' },
|
||||
approved: { label: 'Approved', class: 'cl-st-approved' },
|
||||
denied: { label: 'Denied', class: 'cl-st-denied' },
|
||||
closed: { label: 'Closed', class: 'cl-st-closed' },
|
||||
}
|
||||
|
||||
const priorityMeta: Record<string, { label: string; class: string }> = {
|
||||
critical: { label: 'Critical', class: 'cl-pri-critical' },
|
||||
high: { label: 'High', class: 'cl-pri-high' },
|
||||
medium: { label: 'Med', class: 'cl-pri-medium' },
|
||||
low: { label: 'Low', class: 'cl-pri-low' },
|
||||
}
|
||||
|
||||
const carrierPillClass = (s: CarrierStatus) => {
|
||||
const map: Record<string, string> = {
|
||||
fnol_submitted: 'cl-csp-fnol', acknowledged: 'cl-csp-ack', investigation: 'cl-csp-inv',
|
||||
documentation_pending: 'cl-csp-doc', reserved: 'cl-csp-rsv', negotiation: 'cl-csp-neg',
|
||||
settlement_offered: 'cl-csp-set', closed: 'cl-csp-closed',
|
||||
}
|
||||
return map[s] ?? ''
|
||||
}
|
||||
|
||||
function formatCurrency(n: number) {
|
||||
return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 0 })
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
statusFilter.value = ''
|
||||
carrierFilter.value = ''
|
||||
lobFilter.value = ''
|
||||
handlerFilter.value = ''
|
||||
agingFilter.value = ''
|
||||
priorityFilter.value = ''
|
||||
}
|
||||
|
||||
const hasActiveFilters = computed(() => !!(statusFilter.value || carrierFilter.value || lobFilter.value || handlerFilter.value || agingFilter.value || priorityFilter.value))
|
||||
|
||||
const toast = useToast()
|
||||
function handleNewClaim() {
|
||||
toast.add({ title: 'New claim flow coming soon', description: 'This will open the FNOL intake wizard.', color: 'neutral' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cl-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)]">Claims</h1>
|
||||
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
||||
Track claims lifecycle from first notice of loss through resolution and payment.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="cl-action-btn-primary" @click="handleNewClaim">
|
||||
<UIcon name="i-heroicons-plus" style="width: 14px; height: 14px;" />
|
||||
New Claim
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="cl-kpi-strip">
|
||||
<div class="cl-kpi">
|
||||
<p class="cl-kpi-label">Open claims</p>
|
||||
<p class="cl-kpi-value">{{ kpis.openClaims }}</p>
|
||||
</div>
|
||||
<div class="cl-kpi">
|
||||
<p class="cl-kpi-label">Under review</p>
|
||||
<p class="cl-kpi-value" style="color: #c27b1a;">{{ kpis.underReview }}</p>
|
||||
</div>
|
||||
<div class="cl-kpi">
|
||||
<p class="cl-kpi-label">SLA breached</p>
|
||||
<p class="cl-kpi-value" :style="kpis.breached > 0 ? 'color: #c13838;' : ''">{{ kpis.breached }}</p>
|
||||
</div>
|
||||
<div class="cl-kpi">
|
||||
<p class="cl-kpi-label">Avg days open</p>
|
||||
<p class="cl-kpi-value">{{ kpis.avgDays }}d</p>
|
||||
</div>
|
||||
<div class="cl-kpi">
|
||||
<p class="cl-kpi-label">Total reserved</p>
|
||||
<p class="cl-kpi-value">{{ formatCurrency(kpis.totalReserved) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View toggle + Filter tabs -->
|
||||
<div class="cl-controls-row">
|
||||
<div class="cl-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="cl-view-btn"
|
||||
:class="viewMode === 'my' ? 'cl-view-on' : 'cl-view-off'"
|
||||
@click="viewMode = 'my'"
|
||||
>My Claims</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cl-view-btn"
|
||||
:class="viewMode === 'all' ? 'cl-view-on' : 'cl-view-off'"
|
||||
@click="viewMode = 'all'"
|
||||
>All Claims</button>
|
||||
</div>
|
||||
|
||||
<div class="cl-filter-tabs">
|
||||
<button
|
||||
v-for="f in ([
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'active', label: 'Active' },
|
||||
{ id: 'resolved', label: 'Resolved' },
|
||||
] as { id: ClaimFilter; label: string }[])"
|
||||
:key="f.id"
|
||||
type="button"
|
||||
class="cl-filter-tab"
|
||||
:class="activeFilter === f.id ? 'cl-filter-on' : 'cl-filter-off'"
|
||||
@click="activeFilter = f.id"
|
||||
>
|
||||
{{ f.label }}
|
||||
<span class="cl-filter-count" :class="activeFilter === f.id ? 'cl-filter-count-on' : ''">{{ filterCounts[f.id] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="text-[11px] text-[var(--text-muted)] ml-auto">{{ filteredClaims.length }} results</span>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns row -->
|
||||
<div class="cl-dropdown-row">
|
||||
<select v-model="statusFilter" class="cl-dropdown">
|
||||
<option value="">Status</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="under_review">Under Review</option>
|
||||
<option value="awaiting_docs">Awaiting Docs</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="denied">Denied</option>
|
||||
<option value="closed">Closed</option>
|
||||
</select>
|
||||
<select v-model="carrierFilter" class="cl-dropdown">
|
||||
<option value="">Carrier</option>
|
||||
<option v-for="c in uniqueCarriers" :key="c" :value="c">{{ c }}</option>
|
||||
</select>
|
||||
<select v-model="lobFilter" class="cl-dropdown">
|
||||
<option value="">LOB</option>
|
||||
<option v-for="l in uniqueLobs" :key="l" :value="l">{{ l }}</option>
|
||||
</select>
|
||||
<select v-model="handlerFilter" class="cl-dropdown">
|
||||
<option value="">Handler</option>
|
||||
<option v-for="h in uniqueHandlers" :key="h" :value="h">{{ h }}</option>
|
||||
</select>
|
||||
<select v-model="agingFilter" class="cl-dropdown">
|
||||
<option value="">Aging</option>
|
||||
<option value="0-7">0–7 days</option>
|
||||
<option value="8-14">8–14 days</option>
|
||||
<option value="15-30">15–30 days</option>
|
||||
<option value="30+">30+ days</option>
|
||||
</select>
|
||||
<select v-model="priorityFilter" class="cl-dropdown">
|
||||
<option value="">Priority</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<button v-if="hasActiveFilters" class="cl-clear-btn" @click="clearFilters">
|
||||
<UIcon name="i-heroicons-x-mark" class="w-3 h-3" />
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Claims table -->
|
||||
<div class="cl-table-wrap">
|
||||
<table class="cl-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 28px;"></th>
|
||||
<th>Claim</th>
|
||||
<th>Customer / Agent</th>
|
||||
<th>Line / Type</th>
|
||||
<th>Carrier</th>
|
||||
<th>Status</th>
|
||||
<th class="text-right">Reserved</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Days</th>
|
||||
<th>Priority</th>
|
||||
<th class="text-center">Docs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in filteredClaims"
|
||||
:key="c.id"
|
||||
class="cl-row"
|
||||
:class="{ 'cl-breach-row': c.slaPercent >= 100 && !['closed', 'denied'].includes(c.status) }"
|
||||
style="cursor: pointer;"
|
||||
@click="navigateTo(`/claims/${c.id}`)"
|
||||
>
|
||||
<td><span class="cl-sla-dot" :class="`cl-sla-${slaColor(c.slaPercent)}`" /></td>
|
||||
<td>
|
||||
<NuxtLink :to="`/claims/${c.id}`" class="cl-claim-link" @click.stop>{{ c.id }}</NuxtLink>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ c.customer || 'Unnamed customer' }}</p>
|
||||
<p class="text-[11px] text-[var(--text-muted)]">{{ c.agent || '—' }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-[13px] text-[var(--text-primary)]">{{ c.line }}</p>
|
||||
<p class="text-[11px] text-[var(--text-muted)]">{{ c.type }}</p>
|
||||
</td>
|
||||
<td class="text-[13px] text-[var(--text-muted)]">{{ c.carrier }}</td>
|
||||
<td>
|
||||
<div class="cl-dual-status">
|
||||
<span class="cl-carrier-status-pill" :class="carrierPillClass(c.carrierStatus)">{{ CARRIER_STATUS_LABELS[c.carrierStatus] }}</span>
|
||||
<span class="cl-workflow-status-pill">{{ WORKFLOW_STATUS_LABELS[c.workflowStatus] }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right font-semibold text-[13px] text-[var(--text-primary)]">{{ c.reserved }}</td>
|
||||
<td class="text-right text-[13px]" :class="c.paid !== '$0' ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-muted)] opacity-50'">{{ c.paid }}</td>
|
||||
<td class="text-right">
|
||||
<span class="text-[13px] font-bold" :class="c.daysOpen > 30 ? 'text-rose-600' : c.daysOpen > 14 ? 'text-amber-600' : 'text-[var(--text-primary)]'">{{ c.daysOpen }}d</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="priorityMeta[c.priority].class">{{ priorityMeta[c.priority].label }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span v-if="c.docsPending > 0" class="cl-docs-badge">{{ c.docsPending }}</span>
|
||||
<span v-else class="text-[11px] text-[var(--text-muted)] opacity-40">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cl-page {
|
||||
max-width: 76rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.cl-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;
|
||||
}
|
||||
.cl-action-btn-primary:hover { background: #015458; }
|
||||
|
||||
/* ── KPI strip ── */
|
||||
.cl-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;
|
||||
}
|
||||
.cl-kpi { padding: 14px 18px; background: #fff; }
|
||||
.cl-kpi:first-child { border-radius: 12px 0 0 12px; }
|
||||
.cl-kpi:last-child { border-radius: 0 12px 12px 0; }
|
||||
.cl-kpi-label {
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: #8a8a86;
|
||||
}
|
||||
.cl-kpi-value {
|
||||
margin-top: 4px; font-size: 22px; font-weight: 600;
|
||||
color: var(--text-primary); font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@media (max-width: 640px) { .cl-kpi-strip { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
/* ── Controls row ── */
|
||||
.cl-controls-row {
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── View toggle ── */
|
||||
.cl-view-toggle {
|
||||
display: inline-flex; gap: 1px; padding: 2px;
|
||||
border-radius: 8px; background: rgba(0,0,0,0.04);
|
||||
}
|
||||
.cl-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;
|
||||
}
|
||||
.cl-view-on { background: #01696f; color: white; }
|
||||
.cl-view-off { background: transparent; color: #8a8a86; }
|
||||
.cl-view-off:hover { color: var(--text-primary); }
|
||||
|
||||
/* ── Filter tabs ── */
|
||||
.cl-filter-tabs {
|
||||
display: inline-flex; gap: 2px; padding: 3px;
|
||||
border-radius: 10px; background: rgba(0,0,0,0.04);
|
||||
}
|
||||
.cl-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;
|
||||
}
|
||||
.cl-filter-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.cl-filter-off { background: transparent; color: var(--text-muted); }
|
||||
.cl-filter-off:hover { color: var(--text-primary); }
|
||||
.cl-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);
|
||||
}
|
||||
.cl-filter-count-on { background: rgba(1,105,111,0.1); color: #01696f; }
|
||||
|
||||
/* ── Dropdown filters ── */
|
||||
.cl-dropdown-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.cl-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;
|
||||
}
|
||||
.cl-dropdown:focus { outline: none; border-color: #01696f; }
|
||||
.cl-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;
|
||||
}
|
||||
.cl-clear-btn:hover { background: rgba(193, 56, 56, 0.12); }
|
||||
|
||||
/* ── Table ── */
|
||||
.cl-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;
|
||||
}
|
||||
.cl-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.cl-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;
|
||||
}
|
||||
.cl-table tbody td {
|
||||
padding: 12px 14px; border-bottom: 1px solid rgba(0,0,0,0.04);
|
||||
vertical-align: top;
|
||||
}
|
||||
.cl-row { transition: background 100ms ease; }
|
||||
.cl-row:hover { background: rgba(0,0,0,0.015); }
|
||||
.cl-row:last-child td { border-bottom: none; }
|
||||
|
||||
/* ── Breach row ── */
|
||||
.cl-breach-row { box-shadow: inset 3px 0 0 #c13838; }
|
||||
|
||||
/* ── SLA dot ── */
|
||||
.cl-sla-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.cl-sla-green { background: #059669; }
|
||||
.cl-sla-amber { background: #c27b1a; }
|
||||
.cl-sla-red { background: #c13838; }
|
||||
|
||||
/* ── Claim link ── */
|
||||
.cl-claim-link {
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 12px; font-weight: 600; color: #01696f;
|
||||
text-decoration: none;
|
||||
}
|
||||
.cl-claim-link:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Dual status pills ── */
|
||||
.cl-dual-status { display: flex; flex-direction: column; gap: 3px; }
|
||||
.cl-carrier-status-pill {
|
||||
display: inline-flex; padding: 2px 7px; border-radius: 8px;
|
||||
font-size: 10px; font-weight: 600; white-space: nowrap;
|
||||
}
|
||||
.cl-csp-fnol { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
|
||||
.cl-csp-ack { background: rgba(16, 185, 129, 0.08); color: #059669; }
|
||||
.cl-csp-inv { background: rgba(245, 158, 11, 0.08); color: #d97706; }
|
||||
.cl-csp-doc { background: rgba(147, 51, 234, 0.08); color: #9333ea; }
|
||||
.cl-csp-rsv { background: rgba(1, 105, 111, 0.08); color: #01696f; }
|
||||
.cl-csp-neg { background: rgba(194, 123, 26, 0.08); color: #c27b1a; }
|
||||
.cl-csp-set { background: rgba(16, 185, 129, 0.08); color: #059669; }
|
||||
.cl-csp-closed { background: rgba(138, 138, 134, 0.08); color: #8a8a86; }
|
||||
|
||||
.cl-workflow-status-pill {
|
||||
display: inline-flex; padding: 0; border-radius: 0;
|
||||
font-size: 10px; font-weight: 500; white-space: nowrap;
|
||||
border: none; color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Status badges ── */
|
||||
.cl-st-open { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(193,56,56,0.08); color: #c13838; white-space: nowrap; }
|
||||
.cl-st-review { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(194,123,26,0.08); color: #c27b1a; white-space: nowrap; }
|
||||
.cl-st-docs { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(147,51,234,0.08); color: #9333ea; white-space: nowrap; }
|
||||
.cl-st-approved { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(1,105,111,0.08); color: #01696f; white-space: nowrap; }
|
||||
.cl-st-denied { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(0,0,0,0.06); color: #6b6b68; white-space: nowrap; }
|
||||
.cl-st-closed { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 9999px; background: rgba(0,0,0,0.04); color: #8a8a86; white-space: nowrap; }
|
||||
|
||||
/* ── Priority badges ── */
|
||||
.cl-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; }
|
||||
.cl-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; }
|
||||
.cl-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; }
|
||||
.cl-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; }
|
||||
|
||||
/* ── Docs pending badge ── */
|
||||
.cl-docs-badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 20px; height: 20px; padding: 0 5px;
|
||||
border-radius: 9999px; background: rgba(147,51,234,0.08); color: #9333ea;
|
||||
font-size: 11px; font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
672
app/pages/claims/intake/[token].vue
Normal file
672
app/pages/claims/intake/[token].vue
Normal file
@@ -0,0 +1,672 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ ssr: false, layout: false })
|
||||
|
||||
const route = useRoute()
|
||||
const token = route.params.token as string
|
||||
|
||||
// ── Mock claim lookup by token ────────────────────────────────────────────────
|
||||
interface IntakeClaim {
|
||||
id: string
|
||||
customerName: string
|
||||
policyNumber: string
|
||||
carrier: string
|
||||
lob: 'Auto' | 'Life' | 'General Risk' | 'Home'
|
||||
handler: string
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
const MOCK_TOKENS: Record<string, IntakeClaim> = {
|
||||
'tk_hp_048_a3f1': { id: 'CLM-0048', customerName: 'Hotel Pacífico S.A.', policyNumber: 'PROP-2024-HP-001', carrier: 'ASSA', lob: 'General Risk', handler: 'Ana R.', expiresAt: '2026-04-09T14:30:00Z' },
|
||||
'tk_abc_047_b7e2': { id: 'CLM-0047', customerName: 'Empresa ABC S.A.', policyNumber: 'AUTO-2024-FLEET-007', carrier: 'Qualitas', lob: 'Auto', handler: 'Ana R.', expiresAt: '2026-04-07T11:00:00Z' },
|
||||
'tk_st_043_c9d4': { id: 'CLM-0043', customerName: 'Supermercado Tico S.A.', policyNumber: 'GL-2023-ST-001', carrier: 'Mapfre', lob: 'General Risk', handler: 'Marco V.', expiresAt: '2026-03-20T16:00:00Z' },
|
||||
'demo-auto': { id: 'CLM-DEMO-A', customerName: 'Demo Auto Client', policyNumber: 'AUTO-DEMO-001', carrier: 'ASSA', lob: 'Auto', handler: 'Ana R.', expiresAt: '2026-12-31T23:59:00Z' },
|
||||
'demo-life': { id: 'CLM-DEMO-L', customerName: 'Demo Life Client', policyNumber: 'LIFE-DEMO-001', carrier: 'Pan-American Life', lob: 'Life', handler: 'Ana R.', expiresAt: '2026-12-31T23:59:00Z' },
|
||||
}
|
||||
|
||||
const claim = computed(() => MOCK_TOKENS[token] ?? null)
|
||||
const expired = computed(() => {
|
||||
if (!claim.value) return false
|
||||
return new Date(claim.value.expiresAt) < new Date()
|
||||
})
|
||||
|
||||
// ── Steps ─────────────────────────────────────────────────────────────────────
|
||||
const currentStep = ref(0)
|
||||
const submitted = ref(false)
|
||||
|
||||
const steps = computed(() => {
|
||||
const base = [
|
||||
{ id: 'incident', label: 'Incident Details' },
|
||||
{ id: 'parties', label: claim.value?.lob === 'Auto' ? 'Vehicles & Parties' : claim.value?.lob === 'Life' ? 'Patient & Provider' : 'Property & Parties' },
|
||||
{ id: 'documents', label: 'Documents & Photos' },
|
||||
{ id: 'review', label: 'Review & Submit' },
|
||||
]
|
||||
return base
|
||||
})
|
||||
|
||||
function nextStep() { if (currentStep.value < steps.value.length - 1) currentStep.value++ }
|
||||
function prevStep() { if (currentStep.value > 0) currentStep.value-- }
|
||||
function submitForm() { submitted.value = true }
|
||||
|
||||
// ── Form data ─────────────────────────────────────────────────────────────────
|
||||
const form = reactive({
|
||||
// Step 1: Incident
|
||||
incidentDate: '',
|
||||
incidentTime: '',
|
||||
incidentLocation: '',
|
||||
incidentDescription: '',
|
||||
|
||||
// Step 2: Auto-specific
|
||||
vehicleMake: '',
|
||||
vehicleModel: '',
|
||||
vehicleYear: '',
|
||||
vehiclePlate: '',
|
||||
vehicleColor: '',
|
||||
otherDriverName: '',
|
||||
otherDriverPhone: '',
|
||||
otherDriverInsurance: '',
|
||||
otherDriverPlate: '',
|
||||
witnessName: '',
|
||||
witnessPhone: '',
|
||||
|
||||
// Step 2: Life-specific
|
||||
patientName: '',
|
||||
patientDob: '',
|
||||
patientCedula: '',
|
||||
providerName: '',
|
||||
providerAddress: '',
|
||||
diagnosis: '',
|
||||
treatmentDates: '',
|
||||
|
||||
// Step 2: Property/General Risk
|
||||
propertyAddress: '',
|
||||
propertyType: '',
|
||||
damageDescription: '',
|
||||
emergencyServicesCalled: false,
|
||||
thirdPartyInvolved: false,
|
||||
|
||||
// Step 3: Documents
|
||||
photoDescriptions: [] as string[],
|
||||
hasSignedFud: false,
|
||||
additionalNotes: '',
|
||||
})
|
||||
|
||||
// ── Photo uploads (mock) ──────────────────────────────────────────────────────
|
||||
const photoSlots = computed(() => {
|
||||
if (!claim.value) return []
|
||||
if (claim.value.lob === 'Auto') return [
|
||||
{ id: 'front', label: 'Front of vehicle' },
|
||||
{ id: 'rear', label: 'Rear of vehicle' },
|
||||
{ id: 'left', label: 'Left side' },
|
||||
{ id: 'right', label: 'Right side' },
|
||||
{ id: 'damage', label: 'Close-up of damage' },
|
||||
{ id: 'fud', label: 'FUD firmado — foto del documento (si aplica)', optional: true },
|
||||
]
|
||||
if (claim.value.lob === 'Life') return [
|
||||
{ id: 'prescription', label: 'Medical prescription' },
|
||||
{ id: 'referral', label: 'Specialist referral' },
|
||||
{ id: 'records', label: 'Medical records' },
|
||||
]
|
||||
return [
|
||||
{ id: 'damage1', label: 'Damage photo 1' },
|
||||
{ id: 'damage2', label: 'Damage photo 2' },
|
||||
{ id: 'damage3', label: 'Additional photos' },
|
||||
{ id: 'report', label: 'Fire/police report (if available)', optional: true },
|
||||
]
|
||||
})
|
||||
|
||||
const uploadedPhotos = ref<Record<string, boolean>>({})
|
||||
function mockUpload(slotId: string) { uploadedPhotos.value[slotId] = true }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ci-page">
|
||||
<!-- Segur-OS branding bar -->
|
||||
<div class="ci-brand-bar">
|
||||
<span class="ci-brand-logo">Segur-OS</span>
|
||||
<span class="ci-brand-tag">Client Intake Form</span>
|
||||
</div>
|
||||
|
||||
<!-- Invalid / expired token -->
|
||||
<template v-if="!claim">
|
||||
<div class="ci-error">
|
||||
<div class="ci-error-icon">!</div>
|
||||
<h2>Invalid Link</h2>
|
||||
<p>This intake form link is invalid or has expired. Please contact your broker for a new link.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="expired">
|
||||
<div class="ci-error">
|
||||
<div class="ci-error-icon">⏱</div>
|
||||
<h2>Link Expired</h2>
|
||||
<p>This intake form link expired on {{ new Date(claim.expiresAt).toLocaleDateString() }}. Please contact your broker to request a new link.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Success state -->
|
||||
<template v-else-if="submitted">
|
||||
<div class="ci-success">
|
||||
<div class="ci-success-icon">✓</div>
|
||||
<h2>Thank You</h2>
|
||||
<p>Your claim information for <strong>{{ claim.id }}</strong> has been submitted successfully.</p>
|
||||
<p class="ci-success-sub">Your broker {{ claim.handler }} will review the information and follow up with you shortly.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Main form -->
|
||||
<template v-else>
|
||||
<!-- Claim context header -->
|
||||
<div class="ci-context">
|
||||
<div class="ci-context-left">
|
||||
<h1 class="ci-context-title">{{ claim.customerName }}</h1>
|
||||
<p class="ci-context-meta">{{ claim.id }} · {{ claim.policyNumber }} · {{ claim.carrier }}</p>
|
||||
</div>
|
||||
<div class="ci-context-right">
|
||||
<span class="ci-context-lob">{{ claim.lob }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step indicator -->
|
||||
<div class="ci-steps">
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="step.id"
|
||||
class="ci-step"
|
||||
:class="{
|
||||
'ci-step-done': idx < currentStep,
|
||||
'ci-step-active': idx === currentStep,
|
||||
'ci-step-pending': idx > currentStep,
|
||||
}"
|
||||
>
|
||||
<div class="ci-step-circle">
|
||||
<span v-if="idx < currentStep">✓</span>
|
||||
<span v-else>{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<span class="ci-step-label">{{ step.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step content -->
|
||||
<div class="ci-card">
|
||||
<!-- ═══ Step 1: Incident Details ═══ -->
|
||||
<template v-if="currentStep === 0">
|
||||
<h2 class="ci-section-title">Incident Details</h2>
|
||||
<p class="ci-section-desc">When and where did the incident occur?</p>
|
||||
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Date of Incident</label>
|
||||
<input v-model="form.incidentDate" type="date" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Time (approximate)</label>
|
||||
<input v-model="form.incidentTime" type="time" class="ci-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Location</label>
|
||||
<input v-model="form.incidentLocation" type="text" class="ci-input" placeholder="Street address, intersection, or description" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Description of what happened</label>
|
||||
<textarea v-model="form.incidentDescription" class="ci-textarea" rows="4" placeholder="Describe the incident in detail..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Step 2: Auto ═══ -->
|
||||
<template v-if="currentStep === 1 && claim.lob === 'Auto'">
|
||||
<h2 class="ci-section-title">Vehicles & Parties</h2>
|
||||
<p class="ci-section-desc">Your vehicle and other parties involved.</p>
|
||||
|
||||
<h3 class="ci-subsection">Your Vehicle</h3>
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Make</label>
|
||||
<input v-model="form.vehicleMake" type="text" class="ci-input" placeholder="Toyota" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Model</label>
|
||||
<input v-model="form.vehicleModel" type="text" class="ci-input" placeholder="Hilux" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Year</label>
|
||||
<input v-model="form.vehicleYear" type="text" class="ci-input" placeholder="2024" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Plate</label>
|
||||
<input v-model="form.vehiclePlate" type="text" class="ci-input" placeholder="ABC-123" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Color</label>
|
||||
<input v-model="form.vehicleColor" type="text" class="ci-input" placeholder="White" />
|
||||
</div>
|
||||
|
||||
<h3 class="ci-subsection">Other Driver</h3>
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Name</label>
|
||||
<input v-model="form.otherDriverName" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Phone</label>
|
||||
<input v-model="form.otherDriverPhone" type="tel" class="ci-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Insurance Company</label>
|
||||
<input v-model="form.otherDriverInsurance" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Plate Number</label>
|
||||
<input v-model="form.otherDriverPlate" type="text" class="ci-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="ci-subsection">Witness (if any)</h3>
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Name</label>
|
||||
<input v-model="form.witnessName" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Phone</label>
|
||||
<input v-model="form.witnessPhone" type="tel" class="ci-input" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Step 2: Life ═══ -->
|
||||
<template v-if="currentStep === 1 && claim.lob === 'Life'">
|
||||
<h2 class="ci-section-title">Patient & Provider</h2>
|
||||
<p class="ci-section-desc">Information about the patient and medical provider.</p>
|
||||
|
||||
<h3 class="ci-subsection">Patient Information</h3>
|
||||
<div class="ci-field-grid">
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Patient Name</label>
|
||||
<input v-model="form.patientName" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Date of Birth</label>
|
||||
<input v-model="form.patientDob" type="date" class="ci-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Cédula / ID</label>
|
||||
<input v-model="form.patientCedula" type="text" class="ci-input" />
|
||||
</div>
|
||||
|
||||
<h3 class="ci-subsection">Medical Provider</h3>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Provider / Hospital Name</label>
|
||||
<input v-model="form.providerName" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Provider Address</label>
|
||||
<input v-model="form.providerAddress" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Diagnosis</label>
|
||||
<textarea v-model="form.diagnosis" class="ci-textarea" rows="3" placeholder="Describe the diagnosis or reason for treatment..." />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Treatment Dates</label>
|
||||
<input v-model="form.treatmentDates" type="text" class="ci-input" placeholder="e.g. April 1–5, 2026" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Step 2: General Risk / Home ═══ -->
|
||||
<template v-if="currentStep === 1 && (claim.lob === 'General Risk' || claim.lob === 'Home')">
|
||||
<h2 class="ci-section-title">Property & Parties</h2>
|
||||
<p class="ci-section-desc">Details about the affected property.</p>
|
||||
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Property Address</label>
|
||||
<input v-model="form.propertyAddress" type="text" class="ci-input" />
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Property Type</label>
|
||||
<select v-model="form.propertyType" class="ci-input">
|
||||
<option value="">Select...</option>
|
||||
<option value="commercial">Commercial</option>
|
||||
<option value="residential">Residential</option>
|
||||
<option value="industrial">Industrial</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ci-field">
|
||||
<label class="ci-label">Damage Description</label>
|
||||
<textarea v-model="form.damageDescription" class="ci-textarea" rows="4" placeholder="Describe the damage in detail..." />
|
||||
</div>
|
||||
<div class="ci-field-row">
|
||||
<label class="ci-checkbox">
|
||||
<input v-model="form.emergencyServicesCalled" type="checkbox" />
|
||||
<span>Emergency services were called (fire, police, ambulance)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ci-field-row">
|
||||
<label class="ci-checkbox">
|
||||
<input v-model="form.thirdPartyInvolved" type="checkbox" />
|
||||
<span>Third parties are involved</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Step 3: Documents & Photos ═══ -->
|
||||
<template v-if="currentStep === 2">
|
||||
<h2 class="ci-section-title">Documents & Photos</h2>
|
||||
<p class="ci-section-desc">Upload photos and supporting documents. Take clear, well-lit photos.</p>
|
||||
|
||||
<div class="ci-photo-grid">
|
||||
<div v-for="slot in photoSlots" :key="slot.id" class="ci-photo-slot">
|
||||
<div class="ci-photo-box" :class="{ 'ci-photo-uploaded': uploadedPhotos[slot.id] }" @click="mockUpload(slot.id)">
|
||||
<template v-if="uploadedPhotos[slot.id]">
|
||||
<span class="ci-photo-check">✓</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="ci-photo-plus">+</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="ci-photo-label">{{ slot.label }}</span>
|
||||
<span v-if="slot.optional" class="ci-photo-optional">Optional</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ci-field" style="margin-top: 20px;">
|
||||
<label class="ci-label">Additional Notes</label>
|
||||
<textarea v-model="form.additionalNotes" class="ci-textarea" rows="3" placeholder="Anything else your broker should know..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Step 4: Review & Submit ═══ -->
|
||||
<template v-if="currentStep === 3">
|
||||
<h2 class="ci-section-title">Review & Submit</h2>
|
||||
<p class="ci-section-desc">Please review your information before submitting.</p>
|
||||
|
||||
<div class="ci-review-section">
|
||||
<h3 class="ci-review-heading">Incident</h3>
|
||||
<div class="ci-review-grid">
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Date</span>
|
||||
<span class="ci-review-value">{{ form.incidentDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Time</span>
|
||||
<span class="ci-review-value">{{ form.incidentTime || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item ci-review-full">
|
||||
<span class="ci-review-label">Location</span>
|
||||
<span class="ci-review-value">{{ form.incidentLocation || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item ci-review-full">
|
||||
<span class="ci-review-label">Description</span>
|
||||
<span class="ci-review-value">{{ form.incidentDescription || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="claim.lob === 'Auto'" class="ci-review-section">
|
||||
<h3 class="ci-review-heading">Vehicle</h3>
|
||||
<div class="ci-review-grid">
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Vehicle</span>
|
||||
<span class="ci-review-value">{{ form.vehicleYear }} {{ form.vehicleMake }} {{ form.vehicleModel }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Plate</span>
|
||||
<span class="ci-review-value">{{ form.vehiclePlate || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Other Driver</span>
|
||||
<span class="ci-review-value">{{ form.otherDriverName || 'Not provided' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="claim.lob === 'Life'" class="ci-review-section">
|
||||
<h3 class="ci-review-heading">Patient & Provider</h3>
|
||||
<div class="ci-review-grid">
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Patient</span>
|
||||
<span class="ci-review-value">{{ form.patientName || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Provider</span>
|
||||
<span class="ci-review-value">{{ form.providerName || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item ci-review-full">
|
||||
<span class="ci-review-label">Diagnosis</span>
|
||||
<span class="ci-review-value">{{ form.diagnosis || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="claim.lob === 'General Risk' || claim.lob === 'Home'" class="ci-review-section">
|
||||
<h3 class="ci-review-heading">Property</h3>
|
||||
<div class="ci-review-grid">
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Address</span>
|
||||
<span class="ci-review-value">{{ form.propertyAddress || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item">
|
||||
<span class="ci-review-label">Type</span>
|
||||
<span class="ci-review-value">{{ form.propertyType || '—' }}</span>
|
||||
</div>
|
||||
<div class="ci-review-item ci-review-full">
|
||||
<span class="ci-review-label">Damage</span>
|
||||
<span class="ci-review-value">{{ form.damageDescription || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ci-review-section">
|
||||
<h3 class="ci-review-heading">Documents</h3>
|
||||
<p class="ci-review-photos">{{ Object.values(uploadedPhotos).filter(Boolean).length }} of {{ photoSlots.length }} photos uploaded</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<div class="ci-nav">
|
||||
<button v-if="currentStep > 0" class="ci-btn-back" @click="prevStep">
|
||||
← Back
|
||||
</button>
|
||||
<div class="ci-nav-spacer" />
|
||||
<button v-if="currentStep < steps.length - 1" class="ci-btn-next" @click="nextStep">
|
||||
Continue →
|
||||
</button>
|
||||
<button v-if="currentStep === steps.length - 1" class="ci-btn-submit" @click="submitForm">
|
||||
Submit Claim Information
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="ci-footer">
|
||||
<p>Powered by <strong>Segur-OS</strong> · This form does not require a login</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* =====================================================================
|
||||
CLIENT INTAKE FORM — mobile-first, no layout, ci- prefix
|
||||
===================================================================== */
|
||||
|
||||
.ci-page {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px 48px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: #1a1a1a;
|
||||
min-height: 100vh;
|
||||
background: #f8f8f6;
|
||||
}
|
||||
|
||||
/* ── Brand bar ── */
|
||||
.ci-brand-bar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 16px 0; border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ci-brand-logo { font-size: 16px; font-weight: 800; color: #01696f; letter-spacing: -0.02em; }
|
||||
.ci-brand-tag { font-size: 12px; color: #8a8a86; font-weight: 500; }
|
||||
|
||||
/* ── Error / expired ── */
|
||||
.ci-error { text-align: center; padding: 60px 16px; }
|
||||
.ci-error-icon { font-size: 40px; margin-bottom: 12px; }
|
||||
.ci-error h2 { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
|
||||
.ci-error p { font-size: 14px; color: #5c5650; line-height: 1.6; }
|
||||
|
||||
/* ── Success ── */
|
||||
.ci-success { text-align: center; padding: 60px 16px; }
|
||||
.ci-success-icon {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
background: rgba(1, 105, 111, 0.1); color: #01696f;
|
||||
font-size: 28px; font-weight: 700; margin-bottom: 16px;
|
||||
}
|
||||
.ci-success h2 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
||||
.ci-success p { font-size: 14px; color: #3a3a3a; line-height: 1.6; }
|
||||
.ci-success-sub { color: #8a8a86; margin-top: 8px; }
|
||||
|
||||
/* ── Context header ── */
|
||||
.ci-context {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; padding: 12px 16px;
|
||||
background: white; border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 12px; margin-bottom: 16px;
|
||||
}
|
||||
.ci-context-title { font-size: 16px; font-weight: 700; }
|
||||
.ci-context-meta { font-size: 12px; color: #8a8a86; margin-top: 2px; }
|
||||
.ci-context-lob {
|
||||
display: inline-flex; padding: 4px 10px;
|
||||
border-radius: 8px; font-size: 11px; font-weight: 700;
|
||||
background: rgba(1, 105, 111, 0.08); color: #01696f;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ── Step indicator ── */
|
||||
.ci-steps {
|
||||
display: flex; gap: 4px; margin-bottom: 20px; overflow-x: auto;
|
||||
}
|
||||
.ci-step {
|
||||
display: flex; align-items: center; gap: 6px; padding: 8px 12px;
|
||||
border-radius: 8px; font-size: 12px; font-weight: 500;
|
||||
white-space: nowrap; flex: 1; min-width: 0;
|
||||
}
|
||||
.ci-step-circle {
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.ci-step-label { overflow: hidden; text-overflow: ellipsis; }
|
||||
.ci-step-done { color: #01696f; }
|
||||
.ci-step-done .ci-step-circle { background: #01696f; color: white; }
|
||||
.ci-step-active { color: #1a1a1a; background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
.ci-step-active .ci-step-circle { background: #01696f; color: white; }
|
||||
.ci-step-pending { color: #8a8a86; }
|
||||
.ci-step-pending .ci-step-circle { background: rgba(0,0,0,0.06); color: #8a8a86; }
|
||||
|
||||
/* ── Card ── */
|
||||
.ci-card {
|
||||
background: white; border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 12px; padding: 20px 16px;
|
||||
}
|
||||
|
||||
/* ── Section ── */
|
||||
.ci-section-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
|
||||
.ci-section-desc { font-size: 13px; color: #8a8a86; margin-bottom: 20px; }
|
||||
.ci-subsection { font-size: 14px; font-weight: 600; margin: 20px 0 10px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,0.06); }
|
||||
|
||||
/* ── Fields ── */
|
||||
.ci-field { margin-bottom: 14px; }
|
||||
.ci-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
|
||||
.ci-field-row { margin-bottom: 12px; }
|
||||
.ci-label { display: block; font-size: 12px; font-weight: 600; color: #5c5650; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.03em; }
|
||||
.ci-input {
|
||||
width: 100%; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px; font-size: 14px; color: #1a1a1a;
|
||||
background: white; transition: border-color 150ms ease;
|
||||
}
|
||||
.ci-input:focus { outline: none; border-color: #01696f; }
|
||||
.ci-textarea {
|
||||
width: 100%; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px; font-size: 14px; color: #1a1a1a;
|
||||
resize: vertical; font-family: inherit;
|
||||
}
|
||||
.ci-textarea:focus { outline: none; border-color: #01696f; }
|
||||
.ci-checkbox {
|
||||
display: flex; align-items: flex-start; gap: 8px; cursor: pointer;
|
||||
font-size: 14px; color: #3a3a3a;
|
||||
}
|
||||
.ci-checkbox input { margin-top: 3px; accent-color: #01696f; }
|
||||
|
||||
/* ── Photo grid ── */
|
||||
.ci-photo-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px;
|
||||
}
|
||||
.ci-photo-slot { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.ci-photo-box {
|
||||
width: 100%; aspect-ratio: 4/3; border: 2px dashed rgba(0,0,0,0.12);
|
||||
border-radius: 10px; display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer; transition: all 150ms ease; background: rgba(0,0,0,0.02);
|
||||
}
|
||||
.ci-photo-box:hover { border-color: #01696f; background: rgba(1, 105, 111, 0.03); }
|
||||
.ci-photo-uploaded { border-style: solid; border-color: #01696f; background: rgba(1, 105, 111, 0.06); }
|
||||
.ci-photo-plus { font-size: 24px; color: #8a8a86; }
|
||||
.ci-photo-check { font-size: 24px; color: #01696f; font-weight: 700; }
|
||||
.ci-photo-label { font-size: 11px; color: #5c5650; text-align: center; line-height: 1.3; }
|
||||
.ci-photo-optional { font-size: 10px; color: #8a8a86; font-style: italic; }
|
||||
|
||||
/* ── Review ── */
|
||||
.ci-review-section { margin-bottom: 20px; }
|
||||
.ci-review-heading { font-size: 14px; font-weight: 700; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid rgba(0,0,0,0.06); }
|
||||
.ci-review-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.ci-review-full { grid-column: 1 / -1; }
|
||||
.ci-review-item { }
|
||||
.ci-review-label { display: block; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #8a8a86; margin-bottom: 2px; }
|
||||
.ci-review-value { font-size: 14px; color: #1a1a1a; }
|
||||
.ci-review-photos { font-size: 13px; color: #5c5650; }
|
||||
|
||||
/* ── Navigation ── */
|
||||
.ci-nav {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-top: 16px; padding: 0 4px;
|
||||
}
|
||||
.ci-nav-spacer { flex: 1; }
|
||||
.ci-btn-back {
|
||||
padding: 10px 20px; border-radius: 10px; font-size: 14px; font-weight: 600;
|
||||
background: white; color: #5c5650; border: 1px solid rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ci-btn-back:hover { color: #1a1a1a; border-color: rgba(0,0,0,0.2); }
|
||||
.ci-btn-next {
|
||||
padding: 10px 24px; border-radius: 10px; font-size: 14px; font-weight: 600;
|
||||
background: #01696f; color: white; border: none; cursor: pointer;
|
||||
}
|
||||
.ci-btn-next:hover { opacity: 0.9; }
|
||||
.ci-btn-submit {
|
||||
padding: 12px 28px; border-radius: 10px; font-size: 14px; font-weight: 700;
|
||||
background: #01696f; color: white; border: none; cursor: pointer;
|
||||
}
|
||||
.ci-btn-submit:hover { opacity: 0.9; }
|
||||
|
||||
/* ── Footer ── */
|
||||
.ci-footer {
|
||||
text-align: center; padding: 24px 0; margin-top: 32px;
|
||||
border-top: 1px solid rgba(0,0,0,0.06);
|
||||
font-size: 12px; color: #8a8a86;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 480px) {
|
||||
.ci-field-grid { grid-template-columns: 1fr; }
|
||||
.ci-photo-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.ci-review-grid { grid-template-columns: 1fr; }
|
||||
.ci-steps { gap: 2px; }
|
||||
.ci-step-label { display: none; }
|
||||
}
|
||||
</style>
|
||||
416
app/pages/claims/settings.vue
Normal file
416
app/pages/claims/settings.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Claims Settings')
|
||||
|
||||
// ── SLA Rules ─────────────────────────────────────────────────────────────────
|
||||
interface SlaRule {
|
||||
lob: string
|
||||
targetDays: number
|
||||
tier1Pct: number
|
||||
tier2Pct: number
|
||||
tier3Pct: number
|
||||
}
|
||||
|
||||
const slaRules = ref<SlaRule[]>([
|
||||
{ lob: 'Auto', targetDays: 14, tier1Pct: 50, tier2Pct: 75, tier3Pct: 100 },
|
||||
{ lob: 'Life', targetDays: 21, tier1Pct: 50, tier2Pct: 75, tier3Pct: 100 },
|
||||
{ lob: 'General Risk', targetDays: 30, tier1Pct: 50, tier2Pct: 75, tier3Pct: 100 },
|
||||
{ lob: 'Home', targetDays: 21, tier1Pct: 50, tier2Pct: 75, tier3Pct: 100 },
|
||||
{ lob: 'Fianza', targetDays: 10, tier1Pct: 50, tier2Pct: 75, tier3Pct: 100 },
|
||||
])
|
||||
|
||||
function tierDays(rule: SlaRule, pct: number) {
|
||||
return Math.round(rule.targetDays * pct / 100)
|
||||
}
|
||||
|
||||
// ── Escalation Tiers ──────────────────────────────────────────────────────────
|
||||
interface EscalationTier {
|
||||
threshold: string
|
||||
action: string
|
||||
notify: string
|
||||
}
|
||||
|
||||
const escalationTiers = ref<EscalationTier[]>([
|
||||
{ threshold: '50% of SLA', action: 'Notify handler', notify: 'Handler' },
|
||||
{ threshold: '75% of SLA', action: 'Notify handler + manager', notify: 'Handler, Manager' },
|
||||
{ threshold: '100% of SLA', action: 'Auto-escalate to manager', notify: 'Manager, Team Lead' },
|
||||
])
|
||||
|
||||
// ── Required Document Gates ───────────────────────────────────────────────────
|
||||
interface DocGate {
|
||||
status: string
|
||||
docTypes: string[]
|
||||
}
|
||||
|
||||
const docGateStatuses = ['FNOL Submitted', 'Investigation', 'Documentation Pending', 'Reserved', 'Negotiation', 'Settlement']
|
||||
const docTypes = ['FNOL Form', 'Police Report', 'Photos', 'Estimates', 'Medical Records', 'Proof of Loss', 'Settlement Letter']
|
||||
|
||||
const docGates = ref<Record<string, Record<string, boolean>>>({})
|
||||
|
||||
// Initialize doc gates
|
||||
for (const status of docGateStatuses) {
|
||||
docGates.value[status] = {}
|
||||
for (const doc of docTypes) {
|
||||
// Defaults: FNOL always required, Photos after investigation
|
||||
if (doc === 'FNOL Form') docGates.value[status][doc] = true
|
||||
else if (doc === 'Photos' && ['Investigation', 'Documentation Pending', 'Reserved', 'Negotiation', 'Settlement'].includes(status)) docGates.value[status][doc] = true
|
||||
else if (doc === 'Estimates' && ['Reserved', 'Negotiation', 'Settlement'].includes(status)) docGates.value[status][doc] = true
|
||||
else if (doc === 'Settlement Letter' && status === 'Settlement') docGates.value[status][doc] = true
|
||||
else docGates.value[status][doc] = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDocGate(status: string, doc: string) {
|
||||
docGates.value[status][doc] = !docGates.value[status][doc]
|
||||
}
|
||||
|
||||
// ── Alert Thresholds ──────────────────────────────────────────────────────────
|
||||
const alertThresholds = reactive({
|
||||
reserveIncreasePct: 25,
|
||||
ageDays: 30,
|
||||
carrierNonResponseDays: 5,
|
||||
documentOverdueDays: 7,
|
||||
})
|
||||
|
||||
// ── Form Templates ────────────────────────────────────────────────────────────
|
||||
interface FormTemplate {
|
||||
id: string
|
||||
name: string
|
||||
carrier: string
|
||||
lob: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
const formTemplates = ref<FormTemplate[]>([
|
||||
{ id: 'ft-1', name: 'Informe de Accidente', carrier: 'ASSA', lob: 'Auto', active: true },
|
||||
{ id: 'ft-2', name: 'Informe de Accidente', carrier: 'Qualitas', lob: 'Auto', active: true },
|
||||
{ id: 'ft-3', name: 'Aviso de Pérdida', carrier: 'ASSA', lob: 'General Risk', active: true },
|
||||
{ id: 'ft-4', name: 'Aviso de Pérdida', carrier: 'Mapfre', lob: 'General Risk', active: true },
|
||||
{ id: 'ft-5', name: 'Reclamos Médicos', carrier: 'Pan-American Life', lob: 'Life', active: true },
|
||||
{ id: 'ft-6', name: 'Reclamos Médicos', carrier: 'ASSA', lob: 'Life', active: false },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cs-page">
|
||||
<!-- Header -->
|
||||
<div class="cs-header">
|
||||
<div>
|
||||
<NuxtLink to="/claims" class="cs-back-link">
|
||||
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
|
||||
Back to Claims
|
||||
</NuxtLink>
|
||||
<h1 class="cs-title">Claims Settings</h1>
|
||||
<p class="cs-subtitle">Configure SLA rules, escalation tiers, required documents, and alert thresholds.</p>
|
||||
</div>
|
||||
<button class="cs-save-btn">
|
||||
<UIcon name="i-heroicons-check" class="w-4 h-4" />
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 1: SLA Rule Builder ═══ -->
|
||||
<div class="cs-card">
|
||||
<div class="cs-card-header">
|
||||
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
|
||||
<div>
|
||||
<h2 class="cs-card-title">SLA Rule Builder</h2>
|
||||
<p class="cs-card-desc">Set target resolution days per line of business. Escalation tiers auto-compute from percentages.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-table-wrap">
|
||||
<table class="cs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Line of Business</th>
|
||||
<th>Target Days</th>
|
||||
<th>Tier 1 ({{ slaRules[0]?.tier1Pct ?? 50 }}%)</th>
|
||||
<th>Tier 2 ({{ slaRules[0]?.tier2Pct ?? 75 }}%)</th>
|
||||
<th>Tier 3 ({{ slaRules[0]?.tier3Pct ?? 100 }}%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rule in slaRules" :key="rule.lob">
|
||||
<td class="cs-td-bold">{{ rule.lob }}</td>
|
||||
<td>
|
||||
<input v-model.number="rule.targetDays" type="number" min="1" max="365" class="cs-input-sm" />
|
||||
</td>
|
||||
<td class="cs-td-computed">{{ tierDays(rule, rule.tier1Pct) }} days</td>
|
||||
<td class="cs-td-computed">{{ tierDays(rule, rule.tier2Pct) }} days</td>
|
||||
<td class="cs-td-computed cs-td-red">{{ tierDays(rule, rule.tier3Pct) }} days</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 2: Escalation Tiers ═══ -->
|
||||
<div class="cs-card">
|
||||
<div class="cs-card-header">
|
||||
<UIcon name="i-heroicons-bell-alert" class="w-5 h-5" />
|
||||
<div>
|
||||
<h2 class="cs-card-title">Escalation Tiers</h2>
|
||||
<p class="cs-card-desc">Actions triggered when SLA thresholds are reached.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-escalation-list">
|
||||
<div v-for="(tier, idx) in escalationTiers" :key="idx" class="cs-escalation-row">
|
||||
<div class="cs-escalation-dot" :class="idx === 0 ? 'cs-dot-green' : idx === 1 ? 'cs-dot-amber' : 'cs-dot-red'" />
|
||||
<div class="cs-escalation-content">
|
||||
<span class="cs-escalation-threshold">{{ tier.threshold }}</span>
|
||||
<div class="cs-escalation-fields">
|
||||
<div class="cs-field-inline">
|
||||
<label class="cs-label-sm">Action</label>
|
||||
<input v-model="tier.action" type="text" class="cs-input-med" />
|
||||
</div>
|
||||
<div class="cs-field-inline">
|
||||
<label class="cs-label-sm">Notify</label>
|
||||
<input v-model="tier.notify" type="text" class="cs-input-med" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 3: Required Document Gates ═══ -->
|
||||
<div class="cs-card">
|
||||
<div class="cs-card-header">
|
||||
<UIcon name="i-heroicons-folder-open" class="w-5 h-5" />
|
||||
<div>
|
||||
<h2 class="cs-card-title">Required Document Gates</h2>
|
||||
<p class="cs-card-desc">Check which documents are required at each carrier status stage. Missing docs generate tasks automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-table-wrap cs-matrix-wrap">
|
||||
<table class="cs-table cs-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status ↓ / Doc →</th>
|
||||
<th v-for="doc in docTypes" :key="doc" class="cs-th-rotated">
|
||||
<span>{{ doc }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="status in docGateStatuses" :key="status">
|
||||
<td class="cs-td-bold">{{ status }}</td>
|
||||
<td v-for="doc in docTypes" :key="doc" class="cs-td-check" @click="toggleDocGate(status, doc)">
|
||||
<span v-if="docGates[status][doc]" class="cs-check-on">✓</span>
|
||||
<span v-else class="cs-check-off">·</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 4: Alert Thresholds ═══ -->
|
||||
<div class="cs-card">
|
||||
<div class="cs-card-header">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="w-5 h-5" />
|
||||
<div>
|
||||
<h2 class="cs-card-title">Alert Thresholds</h2>
|
||||
<p class="cs-card-desc">Configure when the system flags claims for attention.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-alert-grid">
|
||||
<div class="cs-alert-item">
|
||||
<label class="cs-label">Reserve Increase Trigger</label>
|
||||
<div class="cs-input-group">
|
||||
<input v-model.number="alertThresholds.reserveIncreasePct" type="number" min="1" max="100" class="cs-input-sm" />
|
||||
<span class="cs-input-suffix">% increase</span>
|
||||
</div>
|
||||
<p class="cs-alert-help">Alert when reserve changes by more than this percentage.</p>
|
||||
</div>
|
||||
<div class="cs-alert-item">
|
||||
<label class="cs-label">Claim Age Warning</label>
|
||||
<div class="cs-input-group">
|
||||
<input v-model.number="alertThresholds.ageDays" type="number" min="1" max="365" class="cs-input-sm" />
|
||||
<span class="cs-input-suffix">days</span>
|
||||
</div>
|
||||
<p class="cs-alert-help">Highlight claims older than this threshold.</p>
|
||||
</div>
|
||||
<div class="cs-alert-item">
|
||||
<label class="cs-label">Carrier Non-Response</label>
|
||||
<div class="cs-input-group">
|
||||
<input v-model.number="alertThresholds.carrierNonResponseDays" type="number" min="1" max="30" class="cs-input-sm" />
|
||||
<span class="cs-input-suffix">days</span>
|
||||
</div>
|
||||
<p class="cs-alert-help">Suggest escalation when carrier hasn't responded.</p>
|
||||
</div>
|
||||
<div class="cs-alert-item">
|
||||
<label class="cs-label">Document Overdue</label>
|
||||
<div class="cs-input-group">
|
||||
<input v-model.number="alertThresholds.documentOverdueDays" type="number" min="1" max="30" class="cs-input-sm" />
|
||||
<span class="cs-input-suffix">days</span>
|
||||
</div>
|
||||
<p class="cs-alert-help">Flag overdue required documents after this many days.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 5: Carrier Form Templates ═══ -->
|
||||
<div class="cs-card">
|
||||
<div class="cs-card-header">
|
||||
<UIcon name="i-heroicons-document-duplicate" class="w-5 h-5" />
|
||||
<div>
|
||||
<h2 class="cs-card-title">Carrier Form Templates</h2>
|
||||
<p class="cs-card-desc">Manage which carrier-specific forms are available for generation. Government forms (FUD) are not managed here.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-table-wrap">
|
||||
<table class="cs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Form Name</th>
|
||||
<th>Carrier</th>
|
||||
<th>LOB</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ft in formTemplates" :key="ft.id">
|
||||
<td class="cs-td-bold">{{ ft.name }}</td>
|
||||
<td>{{ ft.carrier }}</td>
|
||||
<td>{{ ft.lob }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="cs-toggle"
|
||||
:class="ft.active ? 'cs-toggle-on' : 'cs-toggle-off'"
|
||||
@click="ft.active = !ft.active"
|
||||
>
|
||||
<span class="cs-toggle-dot" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* =====================================================================
|
||||
CLAIMS SETTINGS — scoped, cs- prefix
|
||||
===================================================================== */
|
||||
|
||||
.cs-page {
|
||||
max-width: 64rem; margin: 0 auto;
|
||||
display: flex; flex-direction: column; gap: 24px; padding-bottom: 48px;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.cs-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
||||
.cs-back-link {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-size: 12px; font-weight: 500; color: #8a8a86;
|
||||
text-decoration: none; margin-bottom: 8px; transition: color 150ms ease;
|
||||
}
|
||||
.cs-back-link:hover { color: #01696f; }
|
||||
.cs-title { font-size: 22px; font-weight: 700; color: #1a1a1a; }
|
||||
.cs-subtitle { font-size: 13px; color: #8a8a86; margin-top: 4px; }
|
||||
.cs-save-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px;
|
||||
border-radius: 10px; font-size: 13px; font-weight: 600;
|
||||
background: #01696f; color: white; border: none; cursor: pointer;
|
||||
}
|
||||
.cs-save-btn:hover { opacity: 0.9; }
|
||||
|
||||
/* ── Card ── */
|
||||
.cs-card {
|
||||
background: #fff; border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
}
|
||||
.cs-card-header { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; color: #01696f; }
|
||||
.cs-card-title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
||||
.cs-card-desc { font-size: 12px; color: #8a8a86; margin-top: 2px; }
|
||||
|
||||
/* ── Table ── */
|
||||
.cs-table-wrap { overflow-x: auto; }
|
||||
.cs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.cs-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 rgba(0,0,0,0.06);
|
||||
text-align: left; white-space: nowrap;
|
||||
}
|
||||
.cs-table tbody td {
|
||||
padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cs-table tbody tr:last-child td { border-bottom: none; }
|
||||
.cs-td-bold { font-weight: 600; }
|
||||
.cs-td-computed { color: #5c5650; font-variant-numeric: tabular-nums; }
|
||||
.cs-td-red { color: #c13838; font-weight: 600; }
|
||||
|
||||
/* ── Inputs ── */
|
||||
.cs-input-sm {
|
||||
width: 72px; padding: 5px 8px; border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 6px; font-size: 13px; text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.cs-input-sm:focus { outline: none; border-color: #01696f; }
|
||||
.cs-input-med {
|
||||
flex: 1; padding: 5px 10px; border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 6px; font-size: 13px;
|
||||
}
|
||||
.cs-input-med:focus { outline: none; border-color: #01696f; }
|
||||
.cs-input-group { display: flex; align-items: center; gap: 6px; }
|
||||
.cs-input-suffix { font-size: 12px; color: #8a8a86; }
|
||||
.cs-label { display: block; font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 4px; }
|
||||
.cs-label-sm { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: #8a8a86; margin-bottom: 2px; }
|
||||
|
||||
/* ── Escalation ── */
|
||||
.cs-escalation-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.cs-escalation-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||
.cs-escalation-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
|
||||
.cs-dot-green { background: #059669; }
|
||||
.cs-dot-amber { background: #c27b1a; }
|
||||
.cs-dot-red { background: #c13838; }
|
||||
.cs-escalation-content { flex: 1; }
|
||||
.cs-escalation-threshold { font-size: 14px; font-weight: 700; display: block; margin-bottom: 8px; }
|
||||
.cs-escalation-fields { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.cs-field-inline { display: flex; flex-direction: column; flex: 1; min-width: 180px; }
|
||||
|
||||
/* ── Document Matrix ── */
|
||||
.cs-matrix-wrap { max-height: 500px; }
|
||||
.cs-matrix th, .cs-matrix td { text-align: center; }
|
||||
.cs-matrix td:first-child, .cs-matrix th:first-child { text-align: left; }
|
||||
.cs-th-rotated span {
|
||||
writing-mode: vertical-lr; transform: rotate(180deg);
|
||||
font-size: 10px; white-space: nowrap;
|
||||
}
|
||||
.cs-td-check { cursor: pointer; padding: 6px 8px !important; }
|
||||
.cs-td-check:hover { background: rgba(1, 105, 111, 0.04); }
|
||||
.cs-check-on { color: #01696f; font-weight: 700; font-size: 16px; }
|
||||
.cs-check-off { color: rgba(0,0,0,0.15); font-size: 20px; }
|
||||
|
||||
/* ── Alert Grid ── */
|
||||
.cs-alert-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.cs-alert-item { }
|
||||
.cs-alert-help { font-size: 11px; color: #8a8a86; margin-top: 4px; }
|
||||
|
||||
/* ── Toggle ── */
|
||||
.cs-toggle {
|
||||
width: 36px; height: 20px; border-radius: 10px; border: none;
|
||||
cursor: pointer; position: relative; transition: background 200ms ease;
|
||||
padding: 0;
|
||||
}
|
||||
.cs-toggle-on { background: #01696f; }
|
||||
.cs-toggle-off { background: rgba(0,0,0,0.15); }
|
||||
.cs-toggle-dot {
|
||||
display: block; width: 16px; height: 16px; border-radius: 50%;
|
||||
background: white; position: absolute; top: 2px;
|
||||
transition: left 200ms ease;
|
||||
}
|
||||
.cs-toggle-on .cs-toggle-dot { left: 18px; }
|
||||
.cs-toggle-off .cs-toggle-dot { left: 2px; }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 640px) {
|
||||
.cs-alert-grid { grid-template-columns: 1fr; }
|
||||
.cs-escalation-fields { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user