Files
policy-ui/app/pages/support/collectivos/[id].vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

2322 lines
72 KiB
Vue

<script setup lang="ts">
import type { DocumentCategory } from '~/composables/useColectivos'
definePageMeta({ ssr: false })
const route = useRoute()
const id = route.params.id as string
const { getAccount } = useColectivos()
/* ── Resolve account ── */
const account = computed(() => getAccount(id) ?? null)
usePageTitle(computed(() => account.value ? `${account.value.name} · Collectivo` : 'Account Not Found'))
/* ── Tabs ── */
const activeTab = ref<'members' | 'inclusions' | 'billing' | 'claims' | 'certificates' | 'amendments' | 'documents' | 'census'>('members')
const tabs = [
{ key: 'members', label: 'Members' },
{ key: 'census', label: 'Census' },
{ key: 'inclusions', label: 'Inclusions & Exclusions' },
{ key: 'billing', label: 'Billing' },
{ key: 'claims', label: 'Claims' },
{ key: 'certificates', label: 'Certificates' },
{ key: 'amendments', label: 'Amendments' },
{ key: 'documents', label: 'Documents' },
] as const
/* ── Members tab ── */
const memberSearch = ref('')
const memberStatusFilter = ref<'all' | string>('all')
const filteredMembers = computed(() => {
if (!account.value) return []
let rows = [...account.value.members]
if (memberStatusFilter.value !== 'all') rows = rows.filter(m => m.status === memberStatusFilter.value)
const t = memberSearch.value.trim().toLowerCase()
if (t) rows = rows.filter(m =>
m.name.toLowerCase().includes(t) ||
m.documentId.toLowerCase().includes(t) ||
m.email.toLowerCase().includes(t) ||
m.department.toLowerCase().includes(t)
)
return rows
})
const enrollmentProgress = computed(() => {
if (!account.value) return { enrolled: 0, total: 0, pct: 0 }
const total = account.value.totalMembers
const enrolled = account.value.activeMembersCount
return { enrolled, total, pct: total > 0 ? Math.round((enrolled / total) * 100) : 0 }
})
/* ── Census Reconciliation ── */
interface CensusRow {
documentId: string
name: string
department: string
role: string
tier: string
dependents: number
}
type ReconciliationStatus = 'matched' | 'new_in_census' | 'missing_from_census' | 'changed'
interface ReconciliationRow {
documentId: string
name: string
status: ReconciliationStatus
currentDept?: string
censusDept?: string
currentRole?: string
censusRole?: string
currentTier?: string
censusTier?: string
currentDeps?: number
censusDeps?: number
changes: string[]
}
const censusFile = ref<File | null>(null)
const censusFileName = ref('')
const censusReconciled = ref<ReconciliationRow[]>([])
const censusStep = ref<'upload' | 'review' | 'applied'>('upload')
function onCensusFileSelect(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
censusFile.value = file
censusFileName.value = file.name
simulateCensusParse()
}
function simulateCensusParse() {
if (!account.value) return
const members = account.value.members
const rows: ReconciliationRow[] = []
for (const m of members) {
if (m.id === members[members.length - 1]?.id) {
rows.push({
documentId: m.documentId, name: m.name, status: 'missing_from_census',
currentDept: m.department, currentRole: m.role,
changes: ['Not found in uploaded census — possible termination'],
})
} else if (m.id === members[2]?.id) {
rows.push({
documentId: m.documentId, name: m.name, status: 'changed',
currentDept: m.department, censusDept: 'Dirección Comercial',
currentRole: m.role, censusRole: m.role,
currentTier: m.tier, censusTier: m.tier,
currentDeps: m.dependents, censusDeps: m.dependents,
changes: [`Department: ${m.department} → Dirección Comercial`],
})
} else {
rows.push({ documentId: m.documentId, name: m.name, status: 'matched', changes: [] })
}
}
rows.push({
documentId: '9.111.222', name: 'Alejandro Núñez', status: 'new_in_census',
censusDept: 'Ventas', censusRole: 'Ejecutivo de Cuenta', censusTier: 'Basic', censusDeps: 1,
changes: ['New employee — requires enrollment'],
})
rows.push({
documentId: '9.333.444', name: 'Camila Ferreira', status: 'new_in_census',
censusDept: 'Tecnología', censusRole: 'QA Analyst', censusTier: 'Basic', censusDeps: 0,
changes: ['New employee — requires enrollment'],
})
censusReconciled.value = rows
censusStep.value = 'review'
}
function resetCensus() {
censusFile.value = null
censusFileName.value = ''
censusReconciled.value = []
censusStep.value = 'upload'
}
const censusStats = computed(() => {
const rows = censusReconciled.value
return {
matched: rows.filter(r => r.status === 'matched').length,
changed: rows.filter(r => r.status === 'changed').length,
newInCensus: rows.filter(r => r.status === 'new_in_census').length,
missing: rows.filter(r => r.status === 'missing_from_census').length,
total: rows.length,
}
})
/* ── Renewal ring ── */
const renewalDays = computed(() => account.value ? daysUntil(account.value.renewalDate) : 0)
const renewalPct = computed(() => {
const d = renewalDays.value
if (d <= 0) return 100
if (d >= 365) return 0
return Math.round(((365 - d) / 365) * 100)
})
const renewalStroke = computed(() => {
const circ = 2 * Math.PI * 38 // radius 38
const pct = renewalPct.value
return { dasharray: `${circ}`, dashoffset: `${circ - (circ * pct) / 100}` }
})
function reconciliationBadgeClass(s: ReconciliationStatus) {
switch (s) {
case 'matched': return 'ga-badge-active'
case 'new_in_census': return 'ga-badge-open'
case 'missing_from_census': return 'ga-badge-excluded'
case 'changed': return 'ga-badge-pending'
}
}
function reconciliationLabel(s: ReconciliationStatus) {
switch (s) {
case 'matched': return 'Matched'
case 'new_in_census': return 'New (Inclusion)'
case 'missing_from_census': return 'Missing (Exclusion)'
case 'changed': return 'Changed'
}
}
/* ── Inclusions & Exclusions ── */
const inclusions = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.type === 'inclusion')
)
const exclusions = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.type === 'exclusion')
)
/* ── Billing ── */
const currentBilling = computed(() => (account.value?.billingCycles ?? [])[0] ?? null)
/* ── Claims ── */
const allClaims = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.type === 'claim')
)
const claimSearch = ref('')
const claimStatusFilter = ref<'all' | string>('all')
const claimPriorityFilter = ref<'all' | string>('all')
const claimSortField = ref<'created' | 'priority' | 'status'>('created')
const claimSortDir = ref<'asc' | 'desc'>('desc')
function toggleClaimSort(field: 'created' | 'priority' | 'status') {
if (claimSortField.value === field) {
claimSortDir.value = claimSortDir.value === 'asc' ? 'desc' : 'asc'
} else {
claimSortField.value = field
claimSortDir.value = field === 'created' ? 'desc' : 'asc'
}
}
const claims = computed(() => {
let rows = [...allClaims.value]
if (claimStatusFilter.value !== 'all') rows = rows.filter(c => c.status === claimStatusFilter.value)
if (claimPriorityFilter.value !== 'all') rows = rows.filter(c => c.priority === claimPriorityFilter.value)
const t = claimSearch.value.trim().toLowerCase()
if (t) rows = rows.filter(c =>
c.id.toLowerCase().includes(t) ||
c.subject.toLowerCase().includes(t) ||
(c.memberName ?? '').toLowerCase().includes(t) ||
c.assignee.toLowerCase().includes(t)
)
const priorityOrder: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }
const statusOrder: Record<string, number> = { open: 0, in_progress: 1, pending_carrier: 2, pending_client: 3, resolved: 4, cancelled: 5 }
rows.sort((a, b) => {
let cmp = 0
if (claimSortField.value === 'created') cmp = new Date(a.created).getTime() - new Date(b.created).getTime()
else if (claimSortField.value === 'priority') cmp = (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
else if (claimSortField.value === 'status') cmp = (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9)
return claimSortDir.value === 'asc' ? cmp : -cmp
})
return rows
})
/* ── Certificates ── */
const certificates = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.type === 'certificate')
)
/* ── Amendments ── */
const amendments = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.type === 'amendment')
)
/* ── Pending tasks (from open service requests) ── */
const openServiceRequests = computed(() =>
(account.value?.serviceRequests ?? []).filter(r => r.status === 'open' || r.status === 'in_progress' || r.status === 'pending_carrier' || r.status === 'pending_client')
)
/* ── Documents ── */
const docCategoryFilter = ref<'all' | DocumentCategory>('all')
const docCategories: { label: string; value: 'all' | DocumentCategory }[] = [
{ label: 'All', value: 'all' },
{ label: 'Policy', value: 'policy' },
{ label: 'Contract', value: 'contract' },
{ label: 'Endorsement', value: 'endorsement' },
{ label: 'Certificate', value: 'certificate' },
{ label: 'Census', value: 'census' },
{ label: 'Siniestralidad', value: 'siniestralidad' },
{ label: 'Amendment', value: 'amendment' },
{ label: 'Enrollment', value: 'enrollment' },
{ label: 'Correspondence', value: 'correspondence' },
{ label: 'Other', value: 'other' },
]
const filteredDocuments = computed(() => {
if (!account.value) return []
if (docCategoryFilter.value === 'all') return account.value.documents
return account.value.documents.filter(d => d.category === docCategoryFilter.value)
})
const docCategoryCounts = computed(() => {
if (!account.value) return {}
const counts: Record<string, number> = {}
for (const d of account.value.documents) {
counts[d.category] = (counts[d.category] ?? 0) + 1
}
return counts
})
/* ── Helpers ── */
function fmtMoney(v: number) {
return v.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 })
}
function fmtDate(d: string) {
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
function fmtDateShort(d: string) {
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
function daysUntil(d: string) {
const diff = new Date(d).getTime() - Date.now()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
function renewalColor(days: number) {
if (days <= 30) return 'ga-renewal-urgent'
if (days <= 90) return 'ga-renewal-warning'
return 'ga-renewal-ok'
}
function statusBadgeClass(s: string) {
switch (s) {
case 'active': case 'paid': case 'reconciled': return 'ga-badge-active'
case 'pending': case 'pending_enrollment': case 'pending_docs': case 'pending_carrier': case 'pending_client': case 'invoiced': case 'upcoming': return 'ga-badge-pending'
case 'excluded': case 'suspended': case 'cancelled': case 'overdue': return 'ga-badge-excluded'
case 'open': case 'quoting': case 'onboarding': case 'renewal_due': return 'ga-badge-open'
case 'in_progress': case 'on_leave': case 'disputed': return 'ga-badge-in-progress'
case 'resolved': return 'ga-badge-resolved'
default: return 'ga-badge-pending'
}
}
function statusLabel(s: string) {
const map: Record<string, string> = {
active: 'Active', pending: 'Pending', pending_enrollment: 'Pending Enrollment',
pending_docs: 'Pending Docs', excluded: 'Excluded', suspended: 'Suspended',
cancelled: 'Cancelled', on_leave: 'On Leave', open: 'Open', in_progress: 'In Progress',
pending_carrier: 'Pending Carrier', pending_client: 'Pending Client', resolved: 'Resolved',
paid: 'Paid', invoiced: 'Invoiced', overdue: 'Overdue', upcoming: 'Upcoming',
disputed: 'Disputed', reconciled: 'Reconciled',
quoting: 'Quoting', onboarding: 'Onboarding', renewal_due: 'Renewal Due',
}
return map[s] ?? s
}
function priorityDot(p: string) {
if (p === 'urgent' || p === 'high') return 'ga-priority-urgent'
if (p === 'medium' || p === 'normal') return 'ga-priority-normal'
return 'ga-priority-low'
}
function taskStatusIcon(s: string) {
if (s === 'in_progress') return 'i-heroicons-arrow-path'
if (s === 'pending_carrier' || s === 'pending_client') return 'i-heroicons-clock'
return 'i-heroicons-exclamation-circle'
}
function taskStatusColor(s: string) {
if (s === 'in_progress') return 'color: var(--brand)'
if (s === 'pending_carrier' || s === 'pending_client') return 'color: var(--warning)'
return 'color: var(--text-muted)'
}
function activityTypeIcon(t: string) {
const map: Record<string, string> = {
inclusion: 'i-heroicons-user-plus',
exclusion: 'i-heroicons-user-minus',
claim: 'i-heroicons-document-text',
billing: 'i-heroicons-banknotes',
certificate: 'i-heroicons-document-check',
amendment: 'i-heroicons-pencil-square',
document: 'i-heroicons-folder-open',
}
return map[t] ?? 'i-heroicons-bell'
}
function docCategoryBadge(c: DocumentCategory) {
const map: Record<string, string> = {
policy: 'ga-doc-policy', contract: 'ga-doc-contract', endorsement: 'ga-doc-endorsement',
certificate: 'ga-doc-certificate', census: 'ga-doc-census', siniestralidad: 'ga-doc-siniestralidad',
amendment: 'ga-doc-endorsement', enrollment: 'ga-doc-enrollment',
correspondence: 'ga-doc-contract', other: 'ga-doc-other',
}
return map[c] ?? 'ga-doc-other'
}
function docCategoryLabel(c: DocumentCategory) {
const map: Record<string, string> = {
policy: 'Policy', contract: 'Contract', endorsement: 'Endorsement',
certificate: 'Certificate', census: 'Census', siniestralidad: 'Siniestralidad',
amendment: 'Amendment', enrollment: 'Enrollment',
correspondence: 'Correspondence', other: 'Other',
}
return map[c] ?? c
}
</script>
<template>
<!-- Account not found -->
<div v-if="!account" class="ga-not-found">
<UIcon name="i-heroicons-exclamation-triangle" class="ga-not-found-icon" />
<h2 class="ga-not-found-title">Account not found</h2>
<p class="ga-not-found-text">No collectivo account matches ID "{{ id }}".</p>
<NuxtLink to="/support/collectivos" class="ga-not-found-link">
<UIcon name="i-heroicons-arrow-left" class="ga-inline-icon" />
Back to Collectivos
</NuxtLink>
</div>
<!-- Main content -->
<div v-else class="ga-page">
<!-- 1. Back + Header -->
<div class="ga-header-section">
<NuxtLink to="/support/collectivos" class="ga-back-link">
<UIcon name="i-heroicons-arrow-left" class="ga-inline-icon" />
Collectivos
</NuxtLink>
<div class="ga-header-row">
<div class="ga-header-info">
<div class="ga-header-title-row">
<h1 class="ga-company-name">{{ account.name }}</h1>
<span class="ga-lob-badge">{{ account.lob }}</span>
<span class="ga-status-badge" :class="statusBadgeClass(account.status)">{{ statusLabel(account.status) }}</span>
<span class="ga-carrier-name">{{ account.carrier }}</span>
</div>
<p class="ga-subtitle">
{{ account.product }}
<span class="ga-sep">&middot;</span>
RUC {{ account.ruc }}
<span class="ga-sep">&middot;</span>
Agent: {{ account.agent }}
</p>
</div>
<div class="ga-header-actions">
<UButton icon="i-heroicons-pencil-square" color="neutral" variant="soft" size="sm">Edit</UButton>
<UButton icon="i-heroicons-arrow-path" color="neutral" variant="soft" size="sm">Renewal</UButton>
<UButton icon="i-heroicons-document-arrow-down" color="primary" size="sm">Generate Report</UButton>
</div>
</div>
</div>
<!-- 2. Account Summary -->
<div class="ga-summary-card">
<div class="ga-summary-accent" />
<div class="ga-summary-body">
<!-- Left: People cluster -->
<div class="ga-sc-section">
<div class="ga-sc-hero">
<span class="ga-sc-hero-value">{{ account.totalMembers }}</span>
<span class="ga-sc-hero-unit">members</span>
</div>
<div class="ga-sc-sub-row">
<div class="ga-sc-chip">
<span class="ga-sc-chip-dot ga-sc-chip-dot--active" />
<span class="ga-sc-chip-text">{{ account.activeMembersCount }} active</span>
</div>
<div class="ga-sc-chip">
<span class="ga-sc-chip-dot ga-sc-chip-dot--muted" />
<span class="ga-sc-chip-text">{{ account.dependentsCount }} dependents</span>
</div>
<div v-if="account.pendingEnrollment > 0" class="ga-sc-chip ga-sc-chip--warn">
<span class="ga-sc-chip-dot ga-sc-chip-dot--warn" />
<span class="ga-sc-chip-text">{{ account.pendingEnrollment }} pending</span>
</div>
</div>
</div>
<div class="ga-sc-divider" />
<!-- Center: Financials -->
<div class="ga-sc-section ga-sc-section--financials">
<div class="ga-sc-fin-grid">
<div class="ga-sc-fin">
<span class="ga-sc-fin-label">Monthly premium</span>
<span class="ga-sc-fin-value">{{ fmtMoney(account.monthlyPremium) }}</span>
</div>
<div class="ga-sc-fin">
<span class="ga-sc-fin-label">Annual premium</span>
<span class="ga-sc-fin-value">{{ fmtMoney(account.annualPremium) }}</span>
</div>
<div class="ga-sc-fin">
<span class="ga-sc-fin-label">Commission</span>
<span class="ga-sc-fin-value">{{ account.commissionPct }}%</span>
</div>
</div>
</div>
<div class="ga-sc-divider" />
<!-- Right: Renewal ring -->
<div class="ga-sc-section ga-sc-section--renewal">
<div class="ga-sc-ring-wrap">
<svg class="ga-sc-ring" viewBox="0 0 88 88">
<circle cx="44" cy="44" r="38" fill="none" stroke-width="4" class="ga-sc-ring-track" />
<circle
cx="44" cy="44" r="38" fill="none" stroke-width="4"
class="ga-sc-ring-fill"
:class="renewalColor(renewalDays)"
:stroke-dasharray="renewalStroke.dasharray"
:stroke-dashoffset="renewalStroke.dashoffset"
stroke-linecap="round"
transform="rotate(-90 44 44)"
/>
</svg>
<div class="ga-sc-ring-center">
<span class="ga-sc-ring-days" :class="renewalColor(renewalDays)">{{ renewalDays }}</span>
<span class="ga-sc-ring-unit">days</span>
</div>
</div>
<div class="ga-sc-renewal-meta">
<span class="ga-sc-renewal-label">Renewal</span>
<span class="ga-sc-renewal-date">{{ fmtDate(account.renewalDate) }}</span>
</div>
</div>
</div>
</div>
<!-- Main layout: tabs + sidebar -->
<div class="ga-main-layout">
<!-- 3. Tabbed Content -->
<div class="ga-tabs-area">
<!-- Tab toggle -->
<div class="ga-tab-bar">
<button
v-for="tab in tabs"
:key="tab.key"
type="button"
class="ga-tab-btn"
:class="{ 'ga-tab-btn-active': activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<!-- Members Tab -->
<div v-if="activeTab === 'members'" class="ga-tab-panel">
<!-- Enrollment progress -->
<div class="ga-enrollment-bar-card">
<div class="ga-enrollment-header">
<span class="ga-enrollment-text">
{{ enrollmentProgress.enrolled }} of {{ enrollmentProgress.total }} members fully enrolled
<strong>({{ enrollmentProgress.pct }}%)</strong>
</span>
</div>
<div class="ga-progress-track">
<div class="ga-progress-fill" :style="{ width: enrollmentProgress.pct + '%' }" />
</div>
</div>
<!-- Search + filters + actions -->
<div class="ga-toolbar">
<UInput
v-model="memberSearch"
icon="i-heroicons-magnifying-glass"
placeholder="Search members..."
class="ga-toolbar-search"
/>
<div class="ga-toolbar-filter-group">
<button
v-for="opt in ([
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Pending Enrollment', value: 'pending_enrollment' },
{ label: 'Pending Docs', value: 'pending_docs' },
{ label: 'On Leave', value: 'on_leave' },
{ label: 'Excluded', value: 'excluded' },
] as const)"
:key="opt.value"
type="button"
class="ga-filter-chip"
:class="{ 'ga-filter-chip-active': memberStatusFilter === opt.value }"
@click="memberStatusFilter = opt.value"
>
{{ opt.label }}
</button>
</div>
<div class="ga-toolbar-actions">
<UButton icon="i-heroicons-arrow-up-tray" color="neutral" variant="soft" size="sm" @click="activeTab = 'census'">Upload Census</UButton>
<UButton icon="i-heroicons-user-plus" color="primary" size="sm">Add Member</UButton>
</div>
</div>
<!-- Members table -->
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Name</th>
<th>Document ID</th>
<th>Email</th>
<th>Department</th>
<th>Role</th>
<th>Tier</th>
<th class="ga-th-center">Deps</th>
<th>Enrolled</th>
<th>Status</th>
<th class="ga-th-center">Forms</th>
<th class="ga-th-center">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="m in filteredMembers" :key="m.id">
<td class="ga-td-name">
{{ m.name }}
<span v-if="m.pendingDocs.length > 0" class="ga-pending-docs-dot" title="Pending documents" />
</td>
<td class="ga-td-mono">{{ m.documentId }}</td>
<td class="ga-td-small">{{ m.email }}</td>
<td>{{ m.department }}</td>
<td>{{ m.role }}</td>
<td><span class="ga-tier-badge">{{ m.tier }}</span></td>
<td class="ga-td-center">{{ m.dependents }}</td>
<td class="ga-td-small">{{ fmtDateShort(m.enrollmentDate) }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(m.status)">{{ statusLabel(m.status) }}</span></td>
<td class="ga-td-center">
<span :class="m.formsCompleted === m.formsTotal ? 'ga-forms-complete' : 'ga-forms-incomplete'">
{{ m.formsCompleted }}/{{ m.formsTotal }}
</span>
</td>
<td class="ga-td-center">
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-ellipsis-horizontal" />
</td>
</tr>
<tr v-if="filteredMembers.length === 0">
<td colspan="11" class="ga-empty-row">No members match your search.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Census Reconciliation Tab ═══ -->
<div v-if="activeTab === 'census'" class="ga-tab-panel">
<!-- Step 1: Upload -->
<div v-if="censusStep === 'upload'" class="ga-census-upload">
<div class="ga-census-upload-card">
<div class="ga-census-upload-icon-area">
<UIcon name="i-heroicons-arrow-up-tray" class="ga-census-upload-icon" />
</div>
<h3 class="ga-census-upload-title">Upload Monthly Census</h3>
<p class="ga-census-upload-desc">
Upload the employee census (Excel or CSV) provided by the client's HR department.
The system will compare it against the current roster and flag new hires, terminations, and changes.
</p>
<div class="ga-census-upload-formats">
<span class="ga-census-format-badge">XLSX</span>
<span class="ga-census-format-badge">XLS</span>
<span class="ga-census-format-badge">CSV</span>
</div>
<label class="ga-census-upload-btn">
<UIcon name="i-heroicons-folder-open" class="ga-inline-icon" />
Choose file
<input type="file" accept=".xlsx,.xls,.csv" class="sr-only" @change="onCensusFileSelect" />
</label>
<p class="ga-census-upload-hint">Expected columns: Document ID, Name, Department, Role, Tier, Dependents</p>
</div>
<!-- Previous census uploads -->
<div class="ga-census-history">
<h4 class="ga-census-history-title">Previous Census Uploads</h4>
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>File</th>
<th>Period</th>
<th>Uploaded By</th>
<th>Date</th>
<th>Result</th>
<th class="ga-th-center">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ga-td-name">
<UIcon name="i-heroicons-document" class="ga-doc-icon" />
Censo Marzo 2026.xlsx
</td>
<td>March 2026</td>
<td>Silvia Acosta</td>
<td class="ga-td-small">Mar 5</td>
<td>
<span class="ga-status-pill ga-badge-active">3 inclusions, 0 exclusions</span>
</td>
<td class="ga-td-center">
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-down-tray" title="Download" />
</td>
</tr>
<tr>
<td class="ga-td-name">
<UIcon name="i-heroicons-document" class="ga-doc-icon" />
Censo Febrero 2026.xlsx
</td>
<td>February 2026</td>
<td>Silvia Acosta</td>
<td class="ga-td-small">Feb 3</td>
<td>
<span class="ga-status-pill ga-badge-active">2 inclusions, 1 exclusion</span>
</td>
<td class="ga-td-center">
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-down-tray" title="Download" />
</td>
</tr>
<tr>
<td class="ga-td-name">
<UIcon name="i-heroicons-document" class="ga-doc-icon" />
Censo Enero 2026.xlsx
</td>
<td>January 2026</td>
<td>Carlos Villalba</td>
<td class="ga-td-small">Jan 6</td>
<td>
<span class="ga-status-pill ga-badge-active">0 changes</span>
</td>
<td class="ga-td-center">
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-down-tray" title="Download" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Step 2: Review reconciliation -->
<div v-else-if="censusStep === 'review'">
<!-- File info + reset -->
<div class="ga-census-file-bar">
<div class="ga-census-file-info">
<UIcon name="i-heroicons-document-check" class="ga-census-file-icon" />
<div>
<p class="ga-census-file-name">{{ censusFileName }}</p>
<p class="ga-census-file-meta">Parsed {{ censusStats.total }} rows — review differences below</p>
</div>
</div>
<UButton icon="i-heroicons-x-mark" color="neutral" variant="soft" size="sm" @click="resetCensus">Reset</UButton>
</div>
<!-- Reconciliation stats -->
<div class="ga-census-stats">
<div class="ga-census-stat ga-census-stat-matched">
<span class="ga-census-stat-value">{{ censusStats.matched }}</span>
<span class="ga-census-stat-label">Matched</span>
</div>
<div class="ga-census-stat ga-census-stat-changed">
<span class="ga-census-stat-value">{{ censusStats.changed }}</span>
<span class="ga-census-stat-label">Changed</span>
</div>
<div class="ga-census-stat ga-census-stat-new">
<span class="ga-census-stat-value">{{ censusStats.newInCensus }}</span>
<span class="ga-census-stat-label">New (Inclusions)</span>
</div>
<div class="ga-census-stat ga-census-stat-missing">
<span class="ga-census-stat-value">{{ censusStats.missing }}</span>
<span class="ga-census-stat-label">Missing (Exclusions)</span>
</div>
</div>
<!-- Reconciliation table -->
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Document ID</th>
<th>Name</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr v-for="row in censusReconciled" :key="row.documentId" :class="{ 'ga-census-row-action': row.status !== 'matched' }">
<td class="ga-td-mono">{{ row.documentId }}</td>
<td class="ga-td-name">{{ row.name }}</td>
<td>
<span class="ga-status-pill" :class="reconciliationBadgeClass(row.status)">
{{ reconciliationLabel(row.status) }}
</span>
</td>
<td class="ga-td-small">
<template v-if="row.changes.length">
<span v-for="(c, ci) in row.changes" :key="ci" class="ga-census-change-line">{{ c }}</span>
</template>
<span v-else class="ga-census-no-change">No differences</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Action buttons -->
<div class="ga-census-actions">
<UButton color="neutral" variant="soft" @click="resetCensus">Cancel</UButton>
<UButton color="primary" icon="i-heroicons-check" @click="censusStep = 'applied'">
Apply Changes & Generate Requests
</UButton>
</div>
</div>
<!-- Step 3: Applied -->
<div v-else-if="censusStep === 'applied'" class="ga-census-applied">
<div class="ga-census-applied-card">
<div class="ga-census-applied-icon-area">
<UIcon name="i-heroicons-check-circle" class="ga-census-applied-icon" />
</div>
<h3 class="ga-census-upload-title">Census Reconciled</h3>
<p class="ga-census-upload-desc">
{{ censusStats.newInCensus }} inclusion request(s) and {{ censusStats.missing }} exclusion request(s) have been created.
Changed records have been flagged for review.
</p>
<div class="ga-census-applied-actions">
<UButton color="neutral" variant="soft" @click="resetCensus">Upload Another Census</UButton>
<UButton color="primary" variant="soft" @click="activeTab = 'inclusions'">View Inclusions & Exclusions</UButton>
</div>
</div>
</div>
</div>
<!-- ═══ Inclusions & Exclusions Tab ═══ -->
<div v-if="activeTab === 'inclusions'" class="ga-tab-panel">
<!-- Inclusions -->
<div class="ga-ie-section">
<div class="ga-ie-header">
<h3 class="ga-ie-title">
Inclusions
<span class="ga-ie-count">{{ inclusions.length }}</span>
</h3>
<UButton icon="i-heroicons-user-plus" color="primary" size="sm">New Inclusion</UButton>
</div>
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>ID</th>
<th>Member / Subject</th>
<th>Effective Date</th>
<th>Docs Status</th>
<th>Status</th>
<th>Assignee</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr v-for="r in inclusions" :key="r.id">
<td class="ga-td-mono">{{ r.id }}</td>
<td>
<div class="ga-td-name">{{ r.subject }}</div>
<div v-if="r.memberName" class="ga-td-sub">{{ r.memberName }}</div>
</td>
<td>{{ r.effectiveDate ? fmtDateShort(r.effectiveDate) : '\u2014' }}</td>
<td>{{ r.docsStatus ?? '\u2014' }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(r.status)">{{ statusLabel(r.status) }}</span></td>
<td>{{ r.assignee }}</td>
<td class="ga-td-small">{{ fmtDateShort(r.created) }}</td>
</tr>
<tr v-if="inclusions.length === 0">
<td colspan="7" class="ga-empty-row">No inclusion requests.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Exclusions -->
<div class="ga-ie-section">
<div class="ga-ie-header">
<h3 class="ga-ie-title">
Exclusions
<span class="ga-ie-count">{{ exclusions.length }}</span>
</h3>
<UButton icon="i-heroicons-user-minus" color="neutral" variant="soft" size="sm">New Exclusion</UButton>
</div>
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>ID</th>
<th>Member / Subject</th>
<th>Last Day</th>
<th>Reason</th>
<th>Status</th>
<th>Assignee</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<tr v-for="r in exclusions" :key="r.id">
<td class="ga-td-mono">{{ r.id }}</td>
<td>
<div class="ga-td-name">{{ r.subject }}</div>
<div v-if="r.memberName" class="ga-td-sub">{{ r.memberName }}</div>
</td>
<td>{{ r.lastDay ? fmtDateShort(r.lastDay) : '\u2014' }}</td>
<td>{{ r.reason ?? '\u2014' }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(r.status)">{{ statusLabel(r.status) }}</span></td>
<td>{{ r.assignee }}</td>
<td class="ga-td-small">{{ fmtDateShort(r.created) }}</td>
</tr>
<tr v-if="exclusions.length === 0">
<td colspan="7" class="ga-empty-row">No exclusion requests.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ═══ Billing Tab ═══ -->
<div v-if="activeTab === 'billing'" class="ga-tab-panel">
<!-- Current month summary -->
<div v-if="currentBilling" class="ga-billing-summary-card">
<div class="ga-billing-summary-header">
<h3 class="ga-billing-summary-title">Current Billing Period: {{ currentBilling.period }}</h3>
<span class="ga-status-pill" :class="statusBadgeClass(currentBilling.status)">{{ statusLabel(currentBilling.status) }}</span>
</div>
<div class="ga-billing-summary-grid">
<div>
<span class="ga-kpi-label">Invoice Amount</span>
<span class="ga-billing-amount">{{ fmtMoney(currentBilling.invoiceAmount) }}</span>
</div>
<div>
<span class="ga-kpi-label">Paid Amount</span>
<span class="ga-billing-amount">{{ fmtMoney(currentBilling.paidAmount) }}</span>
</div>
<div>
<span class="ga-kpi-label">Due Date</span>
<span class="ga-billing-amount">{{ fmtDate(currentBilling.dueDate) }}</span>
</div>
<div>
<span class="ga-kpi-label">Reconciliation</span>
<span class="ga-billing-amount">
{{ currentBilling.membersBilled }} billed, {{ currentBilling.membersExpected }} expected
<span v-if="currentBilling.membersBilled !== currentBilling.membersExpected" class="ga-discrepancy-flag">
({{ Math.abs(currentBilling.membersExpected - currentBilling.membersBilled) }} discrepancy)
</span>
</span>
</div>
</div>
</div>
<!-- Billing cycles table -->
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Period</th>
<th>Due Date</th>
<th class="ga-th-right">Invoice Amount</th>
<th class="ga-th-right">Paid Amount</th>
<th>Status</th>
<th class="ga-th-center">Billed / Expected</th>
<th>Carrier Ref</th>
</tr>
</thead>
<tbody>
<tr
v-for="bc in (account.billingCycles ?? [])"
:key="bc.id"
:class="{ 'ga-row-discrepancy': bc.membersBilled !== bc.membersExpected || bc.status === 'partial' || bc.status === 'overdue' }"
>
<td class="ga-td-name">{{ bc.period }}</td>
<td>{{ fmtDateShort(bc.dueDate) }}</td>
<td class="ga-td-right ga-td-mono">{{ fmtMoney(bc.invoiceAmount) }}</td>
<td class="ga-td-right ga-td-mono">{{ fmtMoney(bc.paidAmount) }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(bc.status)">{{ statusLabel(bc.status) }}</span></td>
<td class="ga-td-center">
{{ bc.membersBilled }} / {{ bc.membersExpected }}
<UIcon v-if="bc.membersBilled !== bc.membersExpected" name="i-heroicons-exclamation-triangle" class="ga-discrepancy-icon" />
</td>
<td class="ga-td-mono ga-td-small">{{ bc.carrierRef }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Claims Tab ═══ -->
<div v-if="activeTab === 'claims'" class="ga-tab-panel">
<!-- Claims toolbar -->
<div class="ga-toolbar" style="margin-bottom: 12px;">
<UInput
v-model="claimSearch"
icon="i-heroicons-magnifying-glass"
placeholder="Search claims..."
class="ga-toolbar-search"
size="sm"
/>
<select v-model="claimStatusFilter" class="ga-toolbar-select">
<option value="all">All statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="pending_carrier">Pending Carrier</option>
<option value="pending_client">Pending Client</option>
<option value="resolved">Resolved</option>
<option value="cancelled">Cancelled</option>
</select>
<select v-model="claimPriorityFilter" class="ga-toolbar-select">
<option value="all">All priorities</option>
<option value="urgent">Urgent</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<span class="ga-toolbar-count">{{ claims.length }} of {{ allClaims.length }} claims</span>
</div>
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Claim ID</th>
<th>Member</th>
<th>Subject</th>
<th class="ga-th-sortable" @click="toggleClaimSort('status')">
Status
<UIcon v-if="claimSortField === 'status'" :name="claimSortDir === 'asc' ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="ga-sort-icon" />
</th>
<th class="ga-th-sortable" @click="toggleClaimSort('priority')">
Priority
<UIcon v-if="claimSortField === 'priority'" :name="claimSortDir === 'asc' ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="ga-sort-icon" />
</th>
<th class="ga-th-sortable" @click="toggleClaimSort('created')">
Created
<UIcon v-if="claimSortField === 'created'" :name="claimSortDir === 'asc' ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="ga-sort-icon" />
</th>
<th>Assignee</th>
</tr>
</thead>
<tbody>
<tr v-for="c in claims" :key="c.id">
<td class="ga-td-mono">{{ c.id }}</td>
<td class="ga-td-name">{{ c.memberName ?? '\u2014' }}</td>
<td>{{ c.subject }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(c.status)">{{ statusLabel(c.status) }}</span></td>
<td>
<span class="ga-priority-dot" :class="priorityDot(c.priority)" />
{{ c.priority }}
</td>
<td class="ga-td-small">{{ fmtDateShort(c.created) }}</td>
<td>{{ c.assignee }}</td>
</tr>
<tr v-if="claims.length === 0">
<td colspan="7" class="ga-empty-row">No claims match your filters.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Certificates Tab ═══ -->
<div v-if="activeTab === 'certificates'" class="ga-tab-panel">
<div class="ga-toolbar" style="margin-bottom: 16px;">
<div style="flex:1" />
<UButton icon="i-heroicons-document-check" color="primary" size="sm">Generate Certificate</UButton>
</div>
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Request ID</th>
<th>Member</th>
<th>Subject</th>
<th>Status</th>
<th>Created</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr v-for="c in certificates" :key="c.id">
<td class="ga-td-mono">{{ c.id }}</td>
<td class="ga-td-name">{{ c.memberName ?? '\u2014' }}</td>
<td>{{ c.subject }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(c.status)">{{ statusLabel(c.status) }}</span></td>
<td class="ga-td-small">{{ fmtDateShort(c.created) }}</td>
<td class="ga-td-small">{{ c.notes ?? '\u2014' }}</td>
</tr>
<tr v-if="certificates.length === 0">
<td colspan="6" class="ga-empty-row">No certificate requests.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Amendments Tab ═══ -->
<div v-if="activeTab === 'amendments'" class="ga-tab-panel">
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Amendment ID</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Created</th>
<th>Assignee</th>
</tr>
</thead>
<tbody>
<tr v-for="a in amendments" :key="a.id">
<td class="ga-td-mono">{{ a.id }}</td>
<td class="ga-td-name">{{ a.subject }}</td>
<td><span class="ga-status-pill" :class="statusBadgeClass(a.status)">{{ statusLabel(a.status) }}</span></td>
<td>
<span class="ga-priority-dot" :class="priorityDot(a.priority)" />
{{ a.priority }}
</td>
<td class="ga-td-small">{{ fmtDateShort(a.created) }}</td>
<td>{{ a.assignee }}</td>
</tr>
<tr v-if="amendments.length === 0">
<td colspan="6" class="ga-empty-row">No amendments.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ Documents Tab ═══ -->
<div v-if="activeTab === 'documents'" class="ga-tab-panel">
<!-- Category filter tabs -->
<div class="ga-doc-filters">
<button
v-for="cat in docCategories"
:key="cat.value"
type="button"
class="ga-filter-chip"
:class="{ 'ga-filter-chip-active': docCategoryFilter === cat.value }"
@click="docCategoryFilter = cat.value"
>
{{ cat.label }}
<span v-if="cat.value !== 'all' && docCategoryCounts[cat.value]" class="ga-filter-count">
{{ docCategoryCounts[cat.value] }}
</span>
</button>
<div style="flex:1" />
<UButton icon="i-heroicons-arrow-up-tray" color="primary" size="sm">Upload Document</UButton>
</div>
<!-- Document table -->
<div class="ga-table-wrap">
<table class="ga-table">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Type</th>
<th>Size</th>
<th>Uploaded By</th>
<th>Date</th>
<th>Version</th>
<th class="ga-th-center">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="doc in filteredDocuments" :key="doc.id">
<td class="ga-td-name">
<UIcon name="i-heroicons-document" class="ga-doc-icon" />
{{ doc.name }}
</td>
<td><span class="ga-doc-category-badge" :class="docCategoryBadge(doc.category)">{{ docCategoryLabel(doc.category) }}</span></td>
<td class="ga-td-mono">{{ doc.fileType }}</td>
<td class="ga-td-small">{{ doc.fileSize }}</td>
<td>{{ doc.uploadedBy }}</td>
<td class="ga-td-small">{{ fmtDateShort(doc.uploadedAt) }}</td>
<td class="ga-td-center">v{{ doc.version }}</td>
<td class="ga-td-center">
<div class="ga-doc-actions">
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-down-tray" title="Download" />
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-eye" title="View" />
</div>
</td>
</tr>
<tr v-if="filteredDocuments.length === 0">
<td colspan="8" class="ga-empty-row">No documents in this category.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── 4. Right Sidebar ── -->
<aside class="ga-sidebar">
<!-- Contact Info -->
<div class="ga-sidebar-card">
<h4 class="ga-sidebar-card-title">Contact Info</h4>
<div class="ga-contact-block">
<span class="ga-contact-label">Primary Contact</span>
<p class="ga-contact-name">{{ account.contactName }}</p>
<p class="ga-contact-detail">{{ account.contactEmail }}</p>
<p class="ga-contact-detail">{{ account.contactPhone }}</p>
</div>
<div class="ga-contact-divider" />
<div class="ga-contact-block">
<span class="ga-contact-label">HR Contact</span>
<p class="ga-contact-name">{{ account.hrContactName }}</p>
<p class="ga-contact-detail">{{ account.hrContactEmail }}</p>
</div>
</div>
<!-- Recent Activity -->
<div class="ga-sidebar-card">
<h4 class="ga-sidebar-card-title">Recent Activity</h4>
<div class="ga-activity-list">
<div v-for="(evt, i) in account.recentActivity" :key="i" class="ga-activity-item">
<UIcon :name="activityTypeIcon(evt.type)" class="ga-activity-icon" />
<div class="ga-activity-content">
<p class="ga-activity-text">{{ evt.text }}</p>
<p class="ga-activity-date">{{ fmtDateShort(evt.date) }}</p>
</div>
</div>
</div>
</div>
<!-- Pending Tasks -->
<div class="ga-sidebar-card">
<h4 class="ga-sidebar-card-title">Pending Tasks</h4>
<div class="ga-tasks-list">
<div v-for="(sr, i) in openServiceRequests" :key="i" class="ga-task-item">
<UIcon :name="taskStatusIcon(sr.status)" class="ga-task-icon" :style="taskStatusColor(sr.status)" />
<span class="ga-task-text">{{ sr.subject }}</span>
</div>
<p v-if="openServiceRequests.length === 0" class="ga-td-small" style="color: var(--text-muted); font-style: italic;">No pending tasks.</p>
</div>
</div>
</aside>
</div>
</div>
</template>
<style scoped>
/* ── Page layout ── */
.ga-page {
display: flex;
flex-direction: column;
gap: 24px;
padding-bottom: 48px;
}
/* ── Not found ── */
.ga-not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 24px;
text-align: center;
}
.ga-not-found-icon {
width: 48px;
height: 48px;
color: var(--text-muted);
opacity: 0.4;
}
.ga-not-found-title {
margin-top: 16px;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.ga-not-found-text {
margin-top: 8px;
font-size: 14px;
color: var(--text-muted);
}
.ga-not-found-link {
margin-top: 24px;
font-size: 14px;
font-weight: 500;
color: var(--brand);
display: inline-flex;
align-items: center;
gap: 4px;
}
.ga-not-found-link:hover {
text-decoration: underline;
}
/* ── Header ── */
.ga-header-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.ga-back-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
color: var(--brand);
}
.ga-back-link:hover {
text-decoration: underline;
}
.ga-inline-icon {
width: 14px;
height: 14px;
}
.ga-header-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.ga-header-info {
min-width: 0;
flex: 1;
}
.ga-header-title-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.ga-company-name {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.ga-lob-badge {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 10px;
border-radius: 9999px;
background: var(--brand-soft);
color: var(--brand);
}
.ga-carrier-name {
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
}
.ga-subtitle {
margin-top: 4px;
font-size: 14px;
color: var(--text-muted);
line-height: 1.5;
}
.ga-sep {
margin: 0 2px;
opacity: 0.4;
}
.ga-header-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
/* ── Status badges ── */
.ga-status-badge,
.ga-status-pill {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 2px 10px;
border-radius: 9999px;
}
.ga-badge-active {
background: var(--success-soft);
color: var(--success);
}
.ga-badge-pending {
background: var(--warning-soft);
color: var(--warning);
}
.ga-badge-excluded {
background: var(--error-soft);
color: var(--error);
}
.ga-badge-open {
background: var(--brand-soft);
color: var(--brand);
}
.ga-badge-in-progress {
background: var(--info-soft);
color: var(--info);
}
.ga-badge-resolved {
background: var(--success-soft);
color: var(--success);
}
/* ── Account Summary Card ── */
.ga-summary-card {
position: relative;
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
box-shadow: var(--card-shadow);
overflow: hidden;
}
.ga-summary-accent {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--brand), rgba(1, 105, 111, 0.3));
}
.ga-summary-body {
display: flex;
align-items: center;
padding: 20px 28px;
gap: 0;
}
@media (max-width: 900px) {
.ga-summary-body {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
}
.ga-sc-section {
flex: 1;
min-width: 0;
}
.ga-sc-section--financials {
flex: 1.2;
padding: 0 24px;
}
.ga-sc-section--renewal {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 16px;
padding-left: 24px;
}
.ga-sc-divider {
width: 1px;
align-self: stretch;
background: var(--divider);
flex-shrink: 0;
}
/* People hero */
.ga-sc-hero {
display: flex;
align-items: baseline;
gap: 8px;
}
.ga-sc-hero-value {
font-size: 32px;
font-weight: 800;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
line-height: 1;
}
.ga-sc-hero-unit {
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
}
.ga-sc-sub-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.ga-sc-chip {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
}
.ga-sc-chip--warn {
color: var(--warning);
font-weight: 600;
}
.ga-sc-chip-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.ga-sc-chip-dot--active { background: var(--success); }
.ga-sc-chip-dot--muted { background: var(--text-muted); opacity: 0.35; }
.ga-sc-chip-dot--warn { background: var(--warning); }
.ga-sc-chip-text {
white-space: nowrap;
}
/* Financials grid */
.ga-sc-fin-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.ga-sc-fin {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.ga-sc-fin-label {
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
white-space: nowrap;
}
.ga-sc-fin-value {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Renewal ring */
.ga-sc-ring-wrap {
position: relative;
width: 80px;
height: 80px;
flex-shrink: 0;
}
.ga-sc-ring {
width: 100%;
height: 100%;
}
.ga-sc-ring-track {
stroke: var(--divider);
}
.ga-sc-ring-fill {
transition: stroke-dashoffset 600ms cubic-bezier(0.4, 0, 0.2, 1);
}
.ga-sc-ring-fill.ga-renewal-ok { stroke: var(--success); }
.ga-sc-ring-fill.ga-renewal-warning { stroke: var(--warning); }
.ga-sc-ring-fill.ga-renewal-urgent { stroke: var(--error); }
.ga-sc-ring-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ga-sc-ring-days {
font-size: 22px;
font-weight: 800;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.ga-sc-ring-unit {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-top: 1px;
}
.ga-sc-renewal-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.ga-sc-renewal-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.ga-sc-renewal-date {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.ga-renewal-urgent { color: var(--error); }
.ga-renewal-warning { color: var(--warning); }
.ga-renewal-ok { color: var(--success); }
/* Keep ga-kpi-label for billing tab reuse */
.ga-kpi-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
}
/* ── Main layout ── */
.ga-main-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
align-items: start;
}
@media (max-width: 1024px) {
.ga-main-layout {
grid-template-columns: 1fr;
}
}
/* ── Tab bar ── */
.ga-tabs-area {
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.ga-tab-bar {
display: inline-flex;
flex-wrap: wrap;
gap: 2px;
background: rgba(0, 0, 0, 0.04);
border-radius: 10px;
padding: 3px;
}
.ga-tab-btn {
padding: 7px 16px;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
border-radius: 8px;
border: none;
background: transparent;
cursor: pointer;
transition: all 150ms ease;
white-space: nowrap;
}
.ga-tab-btn:hover {
color: var(--text-primary);
}
.ga-tab-btn-active {
background: var(--surface);
color: var(--text-primary);
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* ── Tab panel ── */
.ga-tab-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Enrollment progress ── */
.ga-enrollment-bar-card {
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 16px 20px;
box-shadow: var(--card-shadow);
}
.ga-enrollment-header {
margin-bottom: 10px;
}
.ga-enrollment-text {
font-size: 14px;
color: var(--text-primary);
}
.ga-enrollment-text strong {
color: var(--brand);
}
.ga-progress-track {
height: 8px;
background: rgba(0, 0, 0, 0.06);
border-radius: 9999px;
overflow: hidden;
}
.ga-progress-fill {
height: 100%;
background: var(--brand);
border-radius: 9999px;
transition: width 300ms ease;
}
/* ── Toolbar ── */
.ga-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.ga-toolbar-search {
width: 260px;
}
.ga-toolbar-filter-group {
display: inline-flex;
gap: 2px;
background: rgba(0, 0, 0, 0.04);
border-radius: 8px;
padding: 2px;
}
.ga-toolbar-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.ga-toolbar-select {
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;
}
.ga-toolbar-select:focus {
outline: none;
border-color: #01696f;
}
.ga-toolbar-count {
font-size: 11px;
color: var(--text-muted);
margin-left: auto;
}
.ga-th-sortable {
cursor: pointer;
user-select: none;
}
.ga-th-sortable:hover {
color: var(--text-primary);
}
.ga-sort-icon {
width: 12px;
height: 12px;
vertical-align: middle;
margin-left: 2px;
}
/* ── Filter chips ── */
.ga-filter-chip {
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
border-radius: 6px;
border: none;
background: transparent;
cursor: pointer;
transition: all 150ms ease;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 6px;
}
.ga-filter-chip:hover {
color: var(--text-primary);
}
.ga-filter-chip-active {
background: var(--surface);
color: var(--text-primary);
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.ga-filter-count {
font-size: 10px;
font-weight: 700;
background: var(--brand-soft);
color: var(--brand);
padding: 1px 6px;
border-radius: 9999px;
}
/* ── Tables ── */
.ga-table-wrap {
overflow-x: auto;
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
box-shadow: var(--card-shadow);
}
.ga-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.ga-table thead th {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--divider);
white-space: nowrap;
background: var(--surface);
position: sticky;
top: 0;
}
.ga-th-center {
text-align: center !important;
}
.ga-th-right {
text-align: right !important;
}
.ga-table tbody td {
padding: 10px 16px;
color: var(--text-primary);
border-bottom: 1px solid var(--divider);
vertical-align: middle;
}
.ga-table tbody tr:last-child td {
border-bottom: none;
}
.ga-table tbody tr:hover {
background: var(--brand-faint);
}
.ga-td-name {
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
}
.ga-td-sub {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
.ga-td-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
color: var(--text-muted);
}
.ga-td-small {
font-size: 12px;
color: var(--text-muted);
}
.ga-td-center {
text-align: center;
}
.ga-td-right {
text-align: right;
}
.ga-empty-row {
text-align: center !important;
padding: 32px 16px !important;
color: var(--text-muted) !important;
font-style: italic;
}
/* ── Member specifics ── */
.ga-pending-docs-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--warning);
display: inline-block;
flex-shrink: 0;
}
.ga-tier-badge {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
background: var(--badge-muted-bg);
color: var(--badge-muted-fg);
}
.ga-forms-complete {
color: var(--success);
font-weight: 600;
}
.ga-forms-incomplete {
color: var(--warning);
font-weight: 600;
}
/* ── Inclusions & Exclusions ── */
.ga-ie-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.ga-ie-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.ga-ie-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.ga-ie-count {
font-size: 12px;
font-weight: 700;
background: var(--badge-muted-bg);
color: var(--badge-muted-fg);
padding: 2px 8px;
border-radius: 9999px;
}
/* ── Billing ── */
.ga-billing-summary-card {
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 20px 24px;
box-shadow: var(--card-shadow);
}
.ga-billing-summary-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.ga-billing-summary-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.ga-billing-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
}
.ga-billing-amount {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
margin-top: 4px;
display: block;
}
.ga-discrepancy-flag {
color: var(--error);
font-weight: 700;
font-size: 13px;
}
.ga-discrepancy-icon {
width: 14px;
height: 14px;
color: var(--warning);
margin-left: 4px;
vertical-align: middle;
}
.ga-row-discrepancy {
background: var(--warning-soft);
}
/* ── Priority dots ── */
.ga-priority-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
vertical-align: middle;
}
.ga-priority-urgent {
background: var(--error);
}
.ga-priority-normal {
background: var(--brand);
}
.ga-priority-low {
background: var(--text-muted);
}
/* ── Documents ── */
.ga-doc-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.ga-doc-icon {
width: 16px;
height: 16px;
color: var(--text-muted);
flex-shrink: 0;
}
.ga-doc-category-badge {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 2px 8px;
border-radius: 4px;
text-transform: capitalize;
}
.ga-doc-policy { background: rgba(1, 105, 111, 0.08); color: var(--brand); }
.ga-doc-contract { background: rgba(124, 58, 237, 0.08); color: #7c3aed; }
.ga-doc-endorsement { background: rgba(245, 158, 11, 0.08); color: #b45309; }
.ga-doc-certificate { background: rgba(16, 185, 129, 0.08); color: #047857; }
.ga-doc-census { background: rgba(59, 130, 246, 0.08); color: #2563eb; }
.ga-doc-siniestralidad { background: rgba(239, 68, 68, 0.08); color: #dc2626; }
.ga-doc-enrollment { background: rgba(168, 162, 155, 0.08); color: #6b6b68; }
.ga-doc-other { background: var(--badge-muted-bg); color: var(--badge-muted-fg); }
.ga-doc-actions {
display: flex;
gap: 2px;
justify-content: center;
}
/* ── Sidebar ── */
.ga-sidebar {
display: flex;
flex-direction: column;
gap: 16px;
}
.ga-sidebar-card {
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 20px;
box-shadow: var(--card-shadow);
}
.ga-sidebar-card-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-bottom: 16px;
}
/* Contact */
.ga-contact-block {
display: flex;
flex-direction: column;
gap: 2px;
}
.ga-contact-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-bottom: 4px;
}
.ga-contact-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.ga-contact-detail {
font-size: 13px;
color: var(--text-muted);
}
.ga-contact-divider {
height: 1px;
background: var(--divider);
margin: 14px 0;
}
/* Activity timeline */
.ga-activity-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.ga-activity-item {
display: flex;
gap: 10px;
align-items: flex-start;
}
.ga-activity-icon {
width: 16px;
height: 16px;
color: var(--text-muted);
flex-shrink: 0;
margin-top: 2px;
}
.ga-activity-content {
min-width: 0;
flex: 1;
}
.ga-activity-text {
font-size: 13px;
color: var(--text-primary);
line-height: 1.4;
}
.ga-activity-date {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* Tasks */
.ga-tasks-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.ga-task-item {
display: flex;
gap: 10px;
align-items: flex-start;
}
.ga-task-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-top: 1px;
}
.ga-task-text {
font-size: 13px;
color: var(--text-primary);
line-height: 1.4;
}
/* ── Census Upload ── */
.ga-census-upload {
display: flex;
flex-direction: column;
gap: 24px;
}
.ga-census-upload-card {
background: var(--surface);
border: 2px dashed var(--card-border);
border-radius: 12px;
padding: 40px 32px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.ga-census-upload-icon-area {
width: 56px;
height: 56px;
border-radius: 12px;
background: var(--brand-soft);
display: flex;
align-items: center;
justify-content: center;
}
.ga-census-upload-icon {
width: 28px;
height: 28px;
color: var(--brand);
}
.ga-census-upload-title {
font-size: 17px;
font-weight: 600;
color: var(--text-primary);
}
.ga-census-upload-desc {
font-size: 13px;
color: var(--text-muted);
max-width: 500px;
line-height: 1.5;
}
.ga-census-upload-formats {
display: flex;
gap: 6px;
margin-top: 4px;
}
.ga-census-format-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 8px;
border-radius: 4px;
background: var(--badge-muted-bg);
color: var(--badge-muted-fg);
}
.ga-census-upload-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
font-size: 14px;
font-weight: 600;
color: white;
background: var(--brand);
border-radius: 8px;
cursor: pointer;
transition: opacity 150ms;
margin-top: 8px;
}
.ga-census-upload-btn:hover {
opacity: 0.85;
}
.ga-census-upload-hint {
font-size: 11px;
color: var(--text-muted);
opacity: 0.7;
margin-top: 4px;
}
/* Census history */
.ga-census-history {
display: flex;
flex-direction: column;
gap: 12px;
}
.ga-census-history-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
/* Census file bar */
.ga-census-file-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 14px 20px;
box-shadow: var(--card-shadow);
margin-bottom: 16px;
}
.ga-census-file-info {
display: flex;
align-items: center;
gap: 12px;
}
.ga-census-file-icon {
width: 24px;
height: 24px;
color: var(--brand);
}
.ga-census-file-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.ga-census-file-meta {
font-size: 12px;
color: var(--text-muted);
}
/* Census stats */
.ga-census-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.ga-census-stat {
background: var(--surface);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: var(--card-shadow);
}
.ga-census-stat-value {
display: block;
font-size: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.ga-census-stat-label {
display: block;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-top: 4px;
}
.ga-census-stat-matched .ga-census-stat-value { color: var(--success); }
.ga-census-stat-changed .ga-census-stat-value { color: var(--warning); }
.ga-census-stat-new .ga-census-stat-value { color: var(--brand); }
.ga-census-stat-missing .ga-census-stat-value { color: var(--error); }
/* Census reconciliation rows */
.ga-census-row-action {
background: rgba(245, 158, 11, 0.03);
}
.ga-census-change-line {
display: block;
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
.ga-census-no-change {
font-size: 12px;
color: var(--text-muted);
opacity: 0.5;
}
/* Census actions */
.ga-census-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
}
/* Census applied */
.ga-census-applied {
display: flex;
justify-content: center;
padding: 32px 0;
}
.ga-census-applied-card {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
max-width: 420px;
}
.ga-census-applied-icon-area {
width: 56px;
height: 56px;
border-radius: 12px;
background: var(--success-soft);
display: flex;
align-items: center;
justify-content: center;
}
.ga-census-applied-icon {
width: 28px;
height: 28px;
color: var(--success);
}
.ga-census-applied-actions {
display: flex;
gap: 10px;
margin-top: 8px;
}
</style>