big refactor
This commit is contained in:
@@ -2,464 +2,157 @@
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Colectivos · Cartera')
|
||||
|
||||
const { accounts, activeAccounts, totalMembers, totalDependents, totalPremium } = useColectivos()
|
||||
|
||||
/* ── Filters & sort ── */
|
||||
|
||||
const search = ref('')
|
||||
const viewMode = ref<'card' | 'list'>('card')
|
||||
const lobFilter = ref<string>('all')
|
||||
const statusFilter = ref<string>('all')
|
||||
const carrierFilter = ref<string>('all')
|
||||
const agentFilter = ref<string>('all')
|
||||
const sortBy = ref<string>('premium_desc')
|
||||
|
||||
const lobOptions = [
|
||||
{ label: 'All LOBs', value: 'all' },
|
||||
{ label: 'Health', value: 'Health' },
|
||||
{ label: 'Life', value: 'Life' },
|
||||
{ label: 'Disability', value: 'Disability' },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'All statuses', value: 'all' },
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Onboarding', value: 'onboarding' },
|
||||
{ label: 'Renewal Due', value: 'renewal_due' },
|
||||
{ label: 'Suspended', value: 'suspended' },
|
||||
]
|
||||
|
||||
const sortOptions = [
|
||||
{ label: 'Premium (high → low)', value: 'premium_desc' },
|
||||
{ label: 'Members (high → low)', value: 'members_desc' },
|
||||
{ label: 'Renewal date', value: 'renewal' },
|
||||
{ label: 'Alphabetical', value: 'alpha' },
|
||||
]
|
||||
|
||||
const carrierOptions = computed(() => [
|
||||
{ label: 'All Carriers', value: 'all' },
|
||||
...([...new Set(accounts.value.map(a => a.carrier))].sort().map(c => ({ label: c, value: c })))
|
||||
])
|
||||
|
||||
const agentOptions = computed(() => [
|
||||
{ label: 'All Agents', value: 'all' },
|
||||
...([...new Set(accounts.value.map(a => a.agent))].sort().map(a => ({ label: a, value: a })))
|
||||
])
|
||||
|
||||
/* ── Derived data ── */
|
||||
|
||||
const onboardingCount = computed(() =>
|
||||
accounts.value.filter(a => a.status === 'onboarding').length,
|
||||
)
|
||||
const renewalDueCount = computed(() =>
|
||||
accounts.value.filter(a => a.status === 'renewal_due').length,
|
||||
)
|
||||
const suspendedCount = computed(() =>
|
||||
accounts.value.filter(a => a.status === 'suspended').length,
|
||||
)
|
||||
const totalMonthlyPremium = computed(() =>
|
||||
accounts.value.reduce((s, a) => s + a.monthlyPremium, 0),
|
||||
)
|
||||
const avgCommission = computed(() => {
|
||||
if (!accounts.value.length) return 0
|
||||
return accounts.value.reduce((s, a) => s + a.commissionPct, 0) / accounts.value.length
|
||||
})
|
||||
|
||||
const filtered = computed(() => {
|
||||
let rows = [...accounts.value]
|
||||
|
||||
if (lobFilter.value !== 'all') rows = rows.filter(a => a.lob === lobFilter.value)
|
||||
if (statusFilter.value !== 'all') rows = rows.filter(a => a.status === statusFilter.value)
|
||||
if (carrierFilter.value !== 'all') rows = rows.filter(a => a.carrier === carrierFilter.value)
|
||||
if (agentFilter.value !== 'all') rows = rows.filter(a => a.agent === agentFilter.value)
|
||||
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (q) {
|
||||
rows = rows.filter(a =>
|
||||
a.name.toLowerCase().includes(q) ||
|
||||
a.carrier.toLowerCase().includes(q) ||
|
||||
a.product.toLowerCase().includes(q),
|
||||
)
|
||||
const { data, pending } = usePolicy('/policies', {
|
||||
query: {
|
||||
'page_size': 100
|
||||
}
|
||||
|
||||
switch (sortBy.value) {
|
||||
case 'premium_desc': rows.sort((a, b) => b.annualPremium - a.annualPremium); break
|
||||
case 'members_desc': rows.sort((a, b) => b.totalMembers - a.totalMembers); break
|
||||
case 'renewal': rows.sort((a, b) => a.renewalDate.localeCompare(b.renewalDate)); break
|
||||
case 'alpha': rows.sort((a, b) => a.name.localeCompare(b.name)); break
|
||||
}
|
||||
|
||||
return rows
|
||||
})
|
||||
|
||||
const filteredTotalAnnual = computed(() =>
|
||||
filtered.value.reduce((s, a) => s + a.annualPremium, 0),
|
||||
)
|
||||
const filteredTotalMembers = computed(() =>
|
||||
filtered.value.reduce((s, a) => s + a.totalMembers, 0),
|
||||
)
|
||||
const filteredTotalDependents = computed(() =>
|
||||
filtered.value.reduce((s, a) => s + a.dependentsCount, 0),
|
||||
)
|
||||
|
||||
/* ── Portfolio health segments ── */
|
||||
|
||||
const healthSegments = computed(() => {
|
||||
const total = accounts.value.length || 1
|
||||
const active = activeAccounts.value.length
|
||||
const onboarding = onboardingCount.value
|
||||
const renewal = renewalDueCount.value
|
||||
const suspended = suspendedCount.value
|
||||
return [
|
||||
{ label: 'Active', count: active, pct: (active / total) * 100, color: '#16a34a' },
|
||||
{ label: 'Onboarding', count: onboarding, pct: (onboarding / total) * 100, color: '#3b82f6' },
|
||||
{ label: 'Renewal Due', count: renewal, pct: (renewal / total) * 100, color: '#f59e0b' },
|
||||
{ label: 'Suspended', count: suspended, pct: (suspended / total) * 100, color: '#dc2626' },
|
||||
]
|
||||
})
|
||||
|
||||
/* ── KPI cards config ── */
|
||||
|
||||
const kpiCards = computed(() => [
|
||||
{
|
||||
label: 'Total Groups',
|
||||
value: accounts.value.length.toString(),
|
||||
icon: 'i-heroicons-building-office-2',
|
||||
iconBg: 'rgba(1,105,111,0.08)',
|
||||
iconColor: '#01696f',
|
||||
accent: '',
|
||||
},
|
||||
{
|
||||
label: 'Active Groups',
|
||||
value: activeAccounts.value.length.toString(),
|
||||
icon: 'i-heroicons-check-badge',
|
||||
iconBg: 'rgba(22,163,74,0.08)',
|
||||
iconColor: '#16a34a',
|
||||
accent: '',
|
||||
},
|
||||
{
|
||||
label: 'Total Members',
|
||||
value: totalMembers.value.toLocaleString(),
|
||||
icon: 'i-heroicons-users',
|
||||
iconBg: 'rgba(59,130,246,0.08)',
|
||||
iconColor: '#3b82f6',
|
||||
accent: '',
|
||||
},
|
||||
{
|
||||
label: 'Total Dependents',
|
||||
value: totalDependents.value.toLocaleString(),
|
||||
icon: 'i-heroicons-user-plus',
|
||||
iconBg: 'rgba(139,92,246,0.08)',
|
||||
iconColor: '#8b5cf6',
|
||||
accent: '',
|
||||
},
|
||||
{
|
||||
label: 'Annual Premium',
|
||||
value: fmtCurrency(totalPremium.value),
|
||||
icon: 'i-heroicons-banknotes',
|
||||
iconBg: 'rgba(1,105,111,0.08)',
|
||||
iconColor: '#01696f',
|
||||
accent: 'gc-kpi--teal',
|
||||
},
|
||||
{
|
||||
label: 'Monthly Premium',
|
||||
value: fmtCurrency(totalMonthlyPremium.value),
|
||||
icon: 'i-heroicons-calendar-days',
|
||||
iconBg: 'rgba(1,105,111,0.08)',
|
||||
iconColor: '#01696f',
|
||||
accent: 'gc-kpi--teal',
|
||||
},
|
||||
{
|
||||
label: 'Renewals Due',
|
||||
value: renewalDueCount.value.toString(),
|
||||
icon: 'i-heroicons-clock',
|
||||
iconBg: 'rgba(245,158,11,0.08)',
|
||||
iconColor: '#f59e0b',
|
||||
accent: 'gc-kpi--amber',
|
||||
},
|
||||
{
|
||||
label: 'Onboarding',
|
||||
value: onboardingCount.value.toString(),
|
||||
icon: 'i-heroicons-arrow-path',
|
||||
iconBg: 'rgba(59,130,246,0.08)',
|
||||
iconColor: '#3b82f6',
|
||||
accent: '',
|
||||
},
|
||||
{
|
||||
label: 'Avg Commission',
|
||||
value: avgCommission.value.toFixed(1) + '%',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
iconBg: 'rgba(139,92,246,0.08)',
|
||||
iconColor: '#8b5cf6',
|
||||
accent: '',
|
||||
},
|
||||
])
|
||||
|
||||
/* ── Helpers ── */
|
||||
const policies = computed(() => data.value?.data ?? [])
|
||||
|
||||
function fmtCurrency(n: number) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(n)
|
||||
}
|
||||
|
||||
function fmtDate(d: string) {
|
||||
if (!d) return '—'
|
||||
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
function lobColor(lob: string) {
|
||||
switch (lob) {
|
||||
case 'Health': return 'success'
|
||||
case 'Life': return 'info'
|
||||
case 'Disability': return 'warning'
|
||||
default: return 'neutral'
|
||||
function policyApplicantName(p: any) {
|
||||
const info = p.insured
|
||||
if (!info || typeof info !== 'object') return '—'
|
||||
if (info.type === 'corporate') {
|
||||
return info.company_name || '—'
|
||||
}
|
||||
return info.name || '—'
|
||||
}
|
||||
|
||||
function policyDetailsSummary(p: any) {
|
||||
const d = p.policy_details
|
||||
if (!d || typeof d !== 'object') return '—'
|
||||
if (p.policy_type === 'car') {
|
||||
const parts = [d.year, d.make, d.model].filter((x: any) => x !== undefined && x !== null && String(x) !== '')
|
||||
return parts.length ? parts.map(String).join(' ') : '—'
|
||||
}
|
||||
if (p.policy_type === 'life') {
|
||||
return `Life · ${d.coverage_amount || 0} USD`
|
||||
}
|
||||
if (p.policy_type === 'fire_structure' || p.policy_type === 'fire_contents') {
|
||||
return d.location || '—'
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
function statusColor(status: string) {
|
||||
switch (status) {
|
||||
case 'quote_requested': return 'yellow'
|
||||
case 'quotes_received': return 'blue'
|
||||
case 'solicitation_sent': return 'purple'
|
||||
case 'issued': return 'green'
|
||||
default: return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
function statusBadge(s: string) {
|
||||
switch (s) {
|
||||
case 'active': return { label: 'Active', color: 'success' as const }
|
||||
case 'onboarding': return { label: 'Onboarding', color: 'info' as const }
|
||||
case 'renewal_due': return { label: 'Renewal Due', color: 'warning' as const }
|
||||
case 'quoting': return { label: 'Quoting', color: 'neutral' as const }
|
||||
case 'suspended': return { label: 'Suspended', color: 'error' as const }
|
||||
case 'cancelled': return { label: 'Cancelled', color: 'neutral' as const }
|
||||
default: return { label: s, color: 'neutral' as const }
|
||||
function statusLabel(status: string) {
|
||||
switch (status) {
|
||||
case 'quote_requested': return 'Quote Requested'
|
||||
case 'quotes_received': return 'Quotes Received'
|
||||
case 'solicitation_sent': return 'Solicitation Sent'
|
||||
case 'issued': return 'Issued'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
function renewalClass(d: string) {
|
||||
const diff = (new Date(d).getTime() - Date.now()) / 86_400_000
|
||||
if (diff < 0) return 'gc-renewal--overdue'
|
||||
if (diff <= 30) return 'gc-renewal--urgent'
|
||||
if (diff <= 90) return 'gc-renewal--soon'
|
||||
return ''
|
||||
}
|
||||
|
||||
function renewalDotClass(d: string) {
|
||||
const diff = (new Date(d).getTime() - Date.now()) / 86_400_000
|
||||
if (diff < 0) return 'gc-rdot gc-rdot--red'
|
||||
if (diff <= 30) return 'gc-rdot gc-rdot--orange'
|
||||
if (diff <= 90) return 'gc-rdot gc-rdot--amber'
|
||||
return ''
|
||||
}
|
||||
|
||||
function initials(name: string) {
|
||||
return name.split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase()
|
||||
}
|
||||
|
||||
function initialsColor(name: string) {
|
||||
const colors = ['#01696f', '#3b82f6', '#8b5cf6', '#16a34a', '#ea580c', '#dc2626', '#ca8a04']
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gc-page">
|
||||
<!-- Header -->
|
||||
<div class="gc-header">
|
||||
<div class="gc-header-left">
|
||||
<h1 class="gc-title">Colectivos</h1>
|
||||
<span class="gc-count-badge">{{ filtered.length }}</span>
|
||||
<span class="gc-count-badge">{{ policies.length }}</span>
|
||||
</div>
|
||||
<div class="gc-header-right">
|
||||
<NuxtLink to="/support/collectivos" class="gc-header-link">
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="gc-header-link-icon" />
|
||||
Go to Operations
|
||||
<NuxtLink to="/policies" class="gc-header-link">
|
||||
<UIcon name="i-heroicons-arrow-left" class="gc-header-link-icon" />
|
||||
All Policies
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div class="gc-filters">
|
||||
<UInput
|
||||
v-model="search"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Search group name, carrier, product..."
|
||||
class="gc-filter-search"
|
||||
/>
|
||||
<USelect v-model="lobFilter" :items="lobOptions" value-key="value" label-key="label" class="gc-filter-select" />
|
||||
<USelect v-model="statusFilter" :items="statusOptions" value-key="value" label-key="label" class="gc-filter-select" />
|
||||
<USelect v-model="carrierFilter" :items="carrierOptions" value-key="value" label-key="label" class="gc-filter-select" />
|
||||
<USelect v-model="agentFilter" :items="agentOptions" value-key="value" label-key="label" class="gc-filter-select" />
|
||||
<USelect v-model="sortBy" :items="sortOptions" value-key="value" label-key="label" class="gc-filter-select" />
|
||||
<div class="gc-view-toggle">
|
||||
<button type="button" :class="['gc-view-toggle-btn', viewMode === 'card' && 'gc-view-toggle-btn--active']" title="Card view" @click="viewMode = 'card'">
|
||||
<UIcon name="i-heroicons-squares-2x2" style="width: 16px; height: 16px;" />
|
||||
</button>
|
||||
<button type="button" :class="['gc-view-toggle-btn', viewMode === 'list' && 'gc-view-toggle-btn--active']" title="List view" @click="viewMode = 'list'">
|
||||
<UIcon name="i-heroicons-bars-3" style="width: 16px; height: 16px;" />
|
||||
</button>
|
||||
<div v-if="pending" class="gc-card">
|
||||
<div style="padding: 20px;">
|
||||
<div v-for="n in 8" :key="n" class="pol-skeleton-row">
|
||||
<div class="pol-skeleton" style="width: 80px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 140px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 50px; height: 18px; border-radius: 10px;" />
|
||||
<div class="pol-skeleton" style="width: 100px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 100px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 70px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 50px; height: 18px; border-radius: 10px;" />
|
||||
<div class="pol-skeleton" style="width: 80px; height: 12px;" />
|
||||
<div class="pol-skeleton" style="width: 60px; height: 12px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card View -->
|
||||
<div v-if="viewMode === 'card'" class="gc-card-grid">
|
||||
<NuxtLink
|
||||
v-for="a in filtered"
|
||||
:key="a.id"
|
||||
:to="`/support/collectivos/${a.id}`"
|
||||
class="gc-card-item"
|
||||
>
|
||||
<div class="gc-card-item__top">
|
||||
<span class="gc-avatar" :style="{ background: initialsColor(a.name) + '12', color: initialsColor(a.name) }">{{ initials(a.name) }}</span>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<p class="gc-card-item__name">{{ a.name }}</p>
|
||||
<span class="gc-ruc">{{ a.ruc }}</span>
|
||||
</div>
|
||||
<span class="gc-lob-pill" :class="'gc-lob--' + lobColor(a.lob)">{{ a.lob }}</span>
|
||||
</div>
|
||||
<div class="gc-card-item__body">
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Carrier</span>
|
||||
<span class="gc-card-item__value">{{ a.carrier }}</span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Members</span>
|
||||
<span class="gc-card-item__value">{{ a.totalMembers.toLocaleString() }} <span class="gc-dependents">({{ a.dependentsCount }})</span></span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Annual Premium</span>
|
||||
<span class="gc-card-item__value" style="font-weight: 600;">{{ fmtCurrency(a.annualPremium) }}</span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Commission</span>
|
||||
<span class="gc-card-item__value">{{ a.commissionPct }}%</span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Status</span>
|
||||
<span class="gc-status-pill" :class="'gc-status--' + statusBadge(a.status).color">{{ statusBadge(a.status).label }}</span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Renewal</span>
|
||||
<span :class="renewalClass(a.renewalDate)">{{ fmtDate(a.renewalDate) }}</span>
|
||||
</div>
|
||||
<div class="gc-card-item__field">
|
||||
<span class="gc-card-item__label">Agent</span>
|
||||
<span class="gc-card-item__value">{{ a.agent }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="a.hasUrgentIssues" class="gc-card-item__urgent">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" style="width: 12px; height: 12px;" />
|
||||
Urgent issues
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div v-if="filtered.length === 0" class="gc-empty" style="grid-column: 1 / -1;">No group accounts match your filters.</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div v-else class="gc-table-wrap">
|
||||
<table class="gc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="gc-th gc-th--left">Group Name</th>
|
||||
<th class="gc-th gc-th--left">LOB</th>
|
||||
<th class="gc-th gc-th--left gc-hide-mobile">Carrier</th>
|
||||
<th class="gc-th gc-th--right">Members</th>
|
||||
<th class="gc-th gc-th--right">Annual Premium</th>
|
||||
<th class="gc-th gc-th--right gc-hide-tablet">Monthly Premium</th>
|
||||
<th class="gc-th gc-th--right gc-hide-tablet">Comm %</th>
|
||||
<th class="gc-th gc-th--left">Status</th>
|
||||
<th class="gc-th gc-th--left gc-hide-mobile">Renewal</th>
|
||||
<th class="gc-th gc-th--left gc-hide-tablet">Agent</th>
|
||||
<th class="gc-th gc-th--center" title="Issues">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="gc-th-icon" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in filtered" :key="a.id" class="gc-row">
|
||||
<td class="gc-td">
|
||||
<div class="gc-group-cell">
|
||||
<span
|
||||
class="gc-avatar"
|
||||
:style="{ background: initialsColor(a.name) + '12', color: initialsColor(a.name) }"
|
||||
>{{ initials(a.name) }}</span>
|
||||
<div>
|
||||
<NuxtLink :to="`/support/collectivos/${a.id}`" class="gc-group-link">
|
||||
{{ a.name }}
|
||||
</NuxtLink>
|
||||
<span class="gc-ruc">{{ a.ruc }}</span>
|
||||
<template v-else>
|
||||
<div class="gc-table-wrap">
|
||||
<table class="gc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="gc-th gc-th--left">Applicant</th>
|
||||
<th class="gc-th gc-th--left">Type</th>
|
||||
<th class="gc-th gc-th--left">Details</th>
|
||||
<th class="gc-th gc-th--right">Premium</th>
|
||||
<th class="gc-th gc-th--left">Status</th>
|
||||
<th class="gc-th gc-th--left">Submitted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="policy in policies" :key="policy.application_id" class="gc-row">
|
||||
<td class="gc-td">
|
||||
<div class="gc-group-cell">
|
||||
<span class="gc-avatar">{{ policyApplicantName(policy)[0] }}</span>
|
||||
<div>
|
||||
<NuxtLink :to="`/policies/app/${policy.application_id}`" class="gc-group-link">
|
||||
{{ policyApplicantName(policy) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="gc-td">
|
||||
<span class="gc-lob-pill" :class="'gc-lob--' + lobColor(a.lob)">{{ a.lob }}</span>
|
||||
</td>
|
||||
<td class="gc-td gc-hide-mobile">{{ a.carrier }}</td>
|
||||
<td class="gc-td gc-td--num">
|
||||
{{ a.totalMembers.toLocaleString() }}
|
||||
<span class="gc-dependents">({{ a.dependentsCount }})</span>
|
||||
</td>
|
||||
<td class="gc-td gc-td--num gc-td--premium">{{ fmtCurrency(a.annualPremium) }}</td>
|
||||
<td class="gc-td gc-td--num gc-hide-tablet">{{ fmtCurrency(a.monthlyPremium) }}</td>
|
||||
<td class="gc-td gc-td--num gc-hide-tablet">{{ a.commissionPct }}%</td>
|
||||
<td class="gc-td">
|
||||
<span class="gc-status-pill" :class="'gc-status--' + statusBadge(a.status).color">
|
||||
{{ statusBadge(a.status).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="gc-td gc-hide-mobile" :class="renewalClass(a.renewalDate)">
|
||||
<span :class="renewalDotClass(a.renewalDate)" />
|
||||
{{ fmtDate(a.renewalDate) }}
|
||||
</td>
|
||||
<td class="gc-td gc-hide-tablet">{{ a.agent }}</td>
|
||||
<td class="gc-td gc-td--center">
|
||||
<span
|
||||
v-if="a.hasUrgentIssues"
|
||||
class="gc-dot gc-dot--red"
|
||||
:title="`Urgent issues on ${a.name}`"
|
||||
/>
|
||||
<span
|
||||
v-else-if="a.pendingTasks > 0"
|
||||
class="gc-dot gc-dot--amber"
|
||||
:title="`${a.pendingTasks} pending task${a.pendingTasks !== 1 ? 's' : ''} on ${a.name}`"
|
||||
/>
|
||||
<span v-else class="gc-dot gc-dot--clear" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="filtered.length === 0">
|
||||
<td colspan="11" class="gc-empty">No group accounts match your filters.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 6. Bottom Summary Bar -->
|
||||
<div class="gc-bottom-bar">
|
||||
<div class="gc-bottom-inner">
|
||||
<span class="gc-bottom-item">
|
||||
<UIcon name="i-heroicons-table-cells" class="gc-bottom-icon" />
|
||||
{{ filtered.length }} group{{ filtered.length !== 1 ? 's' : '' }} shown
|
||||
</span>
|
||||
<span class="gc-bottom-sep" />
|
||||
<span class="gc-bottom-item">
|
||||
Annual premium: <strong>{{ fmtCurrency(filteredTotalAnnual) }}</strong>
|
||||
</span>
|
||||
<span class="gc-bottom-sep" />
|
||||
<span class="gc-bottom-item">
|
||||
Members + dependents: <strong>{{ (filteredTotalMembers + filteredTotalDependents).toLocaleString() }}</strong>
|
||||
</span>
|
||||
</td>
|
||||
<td class="gc-td">
|
||||
<span class="gc-lob-pill gc-lob--neutral capitalize">{{ policy.policy_type }}</span>
|
||||
</td>
|
||||
<td class="gc-td">{{ policyDetailsSummary(policy) }}</td>
|
||||
<td class="gc-td gc-td--num gc-td--premium">{{ fmtCurrency(policy.premium || 0) }}</td>
|
||||
<td class="gc-td">
|
||||
<span class="gc-status-pill" :class="`gc-status--${statusColor(policy.status)}`">
|
||||
{{ statusLabel(policy.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="gc-td">{{ fmtDate(policy.submitted_at) }}</td>
|
||||
</tr>
|
||||
<tr v-if="policies.length === 0">
|
||||
<td colspan="6" class="gc-empty">No policies found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. Cross-links -->
|
||||
<div class="gc-crosslinks">
|
||||
<NuxtLink to="/support/collectivos" class="gc-crosslink">
|
||||
Go to Operations
|
||||
<UIcon name="i-heroicons-arrow-right" class="gc-crosslink-icon" />
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/policies" class="gc-crosslink">
|
||||
View All Policies
|
||||
<UIcon name="i-heroicons-arrow-right" class="gc-crosslink-icon" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="gc-bottom-bar">
|
||||
<div class="gc-bottom-inner">
|
||||
<span class="gc-bottom-item">
|
||||
<UIcon name="i-heroicons-table-cells" class="gc-bottom-icon" />
|
||||
{{ policies.length }} polic{{ policies.length !== 1 ? 'ies' : 'y' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── gc- prefix: group cartera scoped styles ── */
|
||||
|
||||
.gc-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -469,8 +162,6 @@ function initialsColor(name: string) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
|
||||
.gc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -490,7 +181,7 @@ function initialsColor(name: string) {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #01696f;
|
||||
background: rgba(1,105,111,0.08);
|
||||
background: rgba(1,105, 111, 0.08);
|
||||
padding: 2px 9px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -522,136 +213,12 @@ function initialsColor(name: string) {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* ── View toggle ── */
|
||||
|
||||
.gc-view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
margin-left: auto;
|
||||
}
|
||||
.gc-view-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8a8a86;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.gc-view-toggle-btn:hover { color: var(--text-primary); }
|
||||
.gc-view-toggle-btn--active {
|
||||
background: #fff;
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ── Card grid ── */
|
||||
|
||||
.gc-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
@media (max-width: 1023px) { .gc-card-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 639px) { .gc-card-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.gc-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
.gc-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gc-card-item:hover {
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.07);
|
||||
border-color: rgba(1, 105, 111, 0.15);
|
||||
}
|
||||
.gc-card-item__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.gc-card-item__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gc-card-item__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.gc-card-item__field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.gc-card-item__label {
|
||||
font-size: 12px;
|
||||
color: #8a8a86;
|
||||
}
|
||||
.gc-card-item__value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.gc-card-item__urgent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
padding: 4px 8px;
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── Filters ── */
|
||||
|
||||
.gc-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gc-filter-search {
|
||||
flex: 1 1 220px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.gc-filter-select {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.gc-filter-select { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Table ── */
|
||||
|
||||
.gc-table-wrap {
|
||||
background: #fff;
|
||||
@@ -681,13 +248,6 @@ function initialsColor(name: string) {
|
||||
|
||||
.gc-th--left { text-align: left; }
|
||||
.gc-th--right { text-align: right; }
|
||||
.gc-th--center { text-align: center; }
|
||||
|
||||
.gc-th-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: #8a8a86;
|
||||
}
|
||||
|
||||
.gc-row {
|
||||
transition: all 150ms ease;
|
||||
@@ -716,12 +276,6 @@ function initialsColor(name: string) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gc-td--center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Group name cell with avatar ── */
|
||||
|
||||
.gc-group-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -740,6 +294,8 @@ function initialsColor(name: string) {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
background: rgba(1, 105, 111, 0.08);
|
||||
color: #01696f;
|
||||
}
|
||||
|
||||
.gc-group-link {
|
||||
@@ -756,21 +312,6 @@ function initialsColor(name: string) {
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.gc-ruc {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #8a8a86;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.gc-dependents {
|
||||
color: #8a8a86;
|
||||
font-size: 11px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* ── Custom pill badges (LOB) ── */
|
||||
|
||||
.gc-lob-pill {
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
@@ -781,28 +322,11 @@ function initialsColor(name: string) {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.gc-lob--success {
|
||||
color: #16a34a;
|
||||
background: rgba(22, 163, 74, 0.1);
|
||||
}
|
||||
|
||||
.gc-lob--info {
|
||||
color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.gc-lob--warning {
|
||||
color: #ca8a04;
|
||||
background: rgba(202, 138, 4, 0.1);
|
||||
}
|
||||
|
||||
.gc-lob--neutral {
|
||||
color: #8a8a86;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Custom pill badges (Status) ── */
|
||||
|
||||
.gc-status-pill {
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
@@ -838,43 +362,6 @@ function initialsColor(name: string) {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Renewal color-coding ── */
|
||||
|
||||
.gc-renewal--overdue { color: #dc2626; font-weight: 600; }
|
||||
.gc-renewal--urgent { color: #ea580c; font-weight: 600; }
|
||||
.gc-renewal--soon { color: #ca8a04; }
|
||||
|
||||
/* ── Renewal date dots ── */
|
||||
|
||||
.gc-rdot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.gc-rdot--red { background: #dc2626; }
|
||||
.gc-rdot--orange { background: #ea580c; }
|
||||
.gc-rdot--amber { background: #f59e0b; }
|
||||
|
||||
/* ── Issues dot ── */
|
||||
|
||||
.gc-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.gc-dot--red { background: #dc2626; }
|
||||
.gc-dot--amber { background: #f59e0b; }
|
||||
.gc-dot--clear { background: transparent; }
|
||||
|
||||
/* ── Empty state ── */
|
||||
|
||||
.gc-empty {
|
||||
padding: 40px 14px;
|
||||
text-align: center;
|
||||
@@ -882,18 +369,6 @@ function initialsColor(name: string) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ── Responsive hide classes ── */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.gc-hide-mobile { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.gc-hide-tablet { display: none; }
|
||||
}
|
||||
|
||||
/* ── Bottom Summary Bar ── */
|
||||
|
||||
.gc-bottom-bar {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
@@ -929,37 +404,22 @@ function initialsColor(name: string) {
|
||||
color: #8a8a86;
|
||||
}
|
||||
|
||||
.gc-bottom-sep {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #d4d4d0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Cross-links ── */
|
||||
|
||||
.gc-crosslinks {
|
||||
.pol-skeleton-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.gc-crosslink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #01696f;
|
||||
text-decoration: none;
|
||||
transition: all 150ms ease;
|
||||
gap: 16px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.gc-crosslink:hover { text-decoration: underline; }
|
||||
|
||||
.gc-crosslink-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
.pol-skeleton-row:last-child { border-bottom: none; }
|
||||
.pol-skeleton {
|
||||
height: 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
animation: pol-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pol-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user