2322 lines
72 KiB
Vue
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">·</span>
|
|
RUC {{ account.ruc }}
|
|
<span class="ga-sep">·</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>
|