WIP jordan

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

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
View 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">07 days</option>
<option value="8-14">814 days</option>
<option value="15-30">1530 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>

View 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 15, 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>

View 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>