big refactor

This commit is contained in:
2026-04-29 16:25:11 -05:00
parent 6c411ce2b6
commit 8265fb689a
156 changed files with 15845 additions and 50373 deletions

View File

@@ -1,14 +1,7 @@
<script setup lang="ts">
import { useCustomerAttention } from '~/composables/useCustomerAttention'
import { MOCK_CUSTOMERS, fmtMoney } from '~/data/mock-customers'
import { customerTier } from '~/data/mock-customers'
definePageMeta({ ssr: false })
usePageTitle('Customer Attention · Settings')
const { config, tiers, rules, getScoreForCustomer, getTierForCustomer } = useCustomerAttention()
/* ── Tabs ── */
type Tab = 'tiers' | 'rules' | 'preview'
const activeTab = ref<Tab>('tiers')
const tabItems: { id: Tab; label: string }[] = [
@@ -17,7 +10,6 @@ const tabItems: { id: Tab; label: string }[] = [
{ id: 'preview', label: 'Preview' },
]
/* ── Operator labels ── */
const operatorLabel: Record<string, string> = {
gte: '>=',
gt: '>',
@@ -26,7 +18,6 @@ const operatorLabel: Record<string, string> = {
eq: '=',
}
/* ── Field labels ── */
const fieldLabel: Record<string, string> = {
premium: 'Annual Premium',
policy_count: 'Policy Count',
@@ -37,41 +28,152 @@ const fieldLabel: Record<string, string> = {
has_private_policies: 'Has Private Policies',
}
/* ── Preview data ── */
const currentYear = new Date().getFullYear()
interface Tier {
id: string
name: string
minScore: number
description: string
color: string
icon: string
benefits: string[]
}
const previewRows = computed(() =>
MOCK_CUSTOMERS.map((c) => {
const activePolicies = c.policies.filter(p => p.status === 'Active' || p.status === 'Pending')
const totalPremium = activePolicies.reduce((sum, p) => sum + p.premium, 0)
const policyCount = activePolicies.length
const lineCount = new Set(activePolicies.map(p => p.line)).size
const sinceYear = c.since ? new Date(c.since).getFullYear() : currentYear
const tenureYears = currentYear - sinceYear
const estimatedCommission = Math.round(totalPremium * 0.15)
interface Rule {
id: string
field: string
operator: string
value: number | boolean
points: number
label: string
}
const input = {
totalPremium,
policyCount,
lineCount,
tenureYears,
isCollectivoMember: false,
hasPrivatePolicies: activePolicies.length > 0,
estimatedCommission,
interface Config {
autoClassify: boolean
}
const config = ref<Config>({ autoClassify: true })
const tiers = ref<Tier[]>([
{
id: 'platinum',
name: 'Platinum',
minScore: 80,
description: 'Highest value customers with premium service',
color: '#7c3aed',
icon: 'i-heroicons-star',
benefits: ['Priority support', 'Dedicated agent', 'Same-day responses']
},
{
id: 'gold',
name: 'Gold',
minScore: 60,
description: 'High-value customers with enhanced service',
color: '#ea580c',
icon: 'i-heroicons-award',
benefits: ['Priority support', '24h response time']
},
{
id: 'silver',
name: 'Silver',
minScore: 40,
description: 'Standard service level',
color: '#64748b',
icon: 'i-heroicons-shield-check',
benefits: ['Standard support', '48h response time']
},
{
id: 'bronze',
name: 'Bronze',
minScore: 0,
description: 'Basic service level',
color: '#78716c',
icon: 'i-heroicons-user',
benefits: ['Email support', '72h response time']
}
])
const rules = ref<Rule[]>([
{ id: '1', field: 'premium', operator: 'gte', value: 100000, points: 20, label: 'High premium' },
{ id: '2', field: 'policy_count', operator: 'gte', value: 5, points: 15, label: 'Multiple policies' },
{ id: '3', field: 'commission', operator: 'gte', value: 15000, points: 15, label: 'High commission' },
{ id: '4', field: 'collectivo_member', operator: 'eq', value: true, points: 10, label: 'Collectivo member' },
{ id: '5', field: 'multi_line', operator: 'gte', value: 3, points: 10, label: 'Multi-line customer' },
{ id: '6', field: 'tenure_years', operator: 'gte', value: 5, points: 10, label: 'Long-term customer' },
{ id: '7', field: 'has_private_policies', operator: 'eq', value: true, points: 5, label: 'Private policies' },
])
function getScoreForCustomer(input: {
totalPremium: number
policyCount: number
lineCount: number
tenureYears: number
isCollectivoMember: boolean
hasPrivatePolicies: boolean
estimatedCommission: number
}): number {
let score = 0
for (const rule of rules.value) {
let matches = false
if (rule.field === 'premium' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.totalPremium >= rule.value) matches = true
else if (rule.operator === 'gt' && input.totalPremium > rule.value) matches = true
else if (rule.operator === 'lte' && input.totalPremium <= rule.value) matches = true
else if (rule.operator === 'lt' && input.totalPremium < rule.value) matches = true
else if (rule.operator === 'eq' && input.totalPremium === rule.value) matches = true
} else if (rule.field === 'policy_count' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.policyCount >= rule.value) matches = true
else if (rule.operator === 'gt' && input.policyCount > rule.value) matches = true
else if (rule.operator === 'lte' && input.policyCount <= rule.value) matches = true
else if (rule.operator === 'lt' && input.policyCount < rule.value) matches = true
else if (rule.operator === 'eq' && input.policyCount === rule.value) matches = true
} else if (rule.field === 'commission' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.estimatedCommission >= rule.value) matches = true
else if (rule.operator === 'gt' && input.estimatedCommission > rule.value) matches = true
else if (rule.operator === 'lte' && input.estimatedCommission <= rule.value) matches = true
else if (rule.operator === 'lt' && input.estimatedCommission < rule.value) matches = true
else if (rule.operator === 'eq' && input.estimatedCommission === rule.value) matches = true
} else if (rule.field === 'collectivo_member' && typeof rule.value === 'boolean') {
if (rule.operator === 'eq' && input.isCollectivoMember === rule.value) matches = true
} else if (rule.field === 'multi_line' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.lineCount >= rule.value) matches = true
else if (rule.operator === 'gt' && input.lineCount > rule.value) matches = true
else if (rule.operator === 'lte' && input.lineCount <= rule.value) matches = true
else if (rule.operator === 'lt' && input.lineCount < rule.value) matches = true
else if (rule.operator === 'eq' && input.lineCount === rule.value) matches = true
} else if (rule.field === 'tenure_years' && typeof rule.value === 'number') {
if (rule.operator === 'gte' && input.tenureYears >= rule.value) matches = true
else if (rule.operator === 'gt' && input.tenureYears > rule.value) matches = true
else if (rule.operator === 'lte' && input.tenureYears <= rule.value) matches = true
else if (rule.operator === 'lt' && input.tenureYears < rule.value) matches = true
else if (rule.operator === 'eq' && input.tenureYears === rule.value) matches = true
} else if (rule.field === 'has_private_policies' && typeof rule.value === 'boolean') {
if (rule.operator === 'eq' && input.hasPrivatePolicies === rule.value) matches = true
}
if (matches) score += rule.points
}
return score
}
return {
id: c.id,
name: c.name,
totalPremium,
policyCount,
lineCount,
tenureYears,
score: getScoreForCustomer(input),
tier: getTierForCustomer(input),
}
}).sort((a, b) => b.score - a.score)
)
function getTierForCustomer(input: {
totalPremium: number
policyCount: number
lineCount: number
tenureYears: number
isCollectivoMember: boolean
hasPrivatePolicies: boolean
estimatedCommission: number
}): Tier {
const score = getScoreForCustomer(input)
for (const tier of tiers.value) {
if (score >= tier.minScore) return tier
}
return tiers.value[tiers.value.length - 1]
}
function fmtMoney(n: number) {
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`
return `$${n.toLocaleString()}`
}
function formatRuleValue(rule: { field: string; value: number | boolean }): string {
if (typeof rule.value === 'boolean') return rule.value ? 'Yes' : 'No'
@@ -209,27 +311,16 @@ function formatRuleValue(rule: { field: string; value: number | boolean }): stri
</tr>
</thead>
<tbody>
<tr v-for="row in previewRows" :key="row.id">
<td class="ca-cell-name">{{ row.name }}</td>
<td>{{ row.totalPremium > 0 ? fmtMoney(row.totalPremium) : '—' }}</td>
<td>{{ row.policyCount }}</td>
<td>{{ row.lineCount }}</td>
<td>{{ row.tenureYears }}yr</td>
<td class="ca-cell-score">{{ row.score }}</td>
<td>
<span
class="ca-tier-badge"
:style="{ background: row.tier.color + '14', color: row.tier.color }"
>
{{ row.tier.name }}
</span>
<tr>
<td colspan="7" class="text-center py-8 text-[var(--text-muted)]">
No preview data available. Connect to API to see customer classification.
</td>
</tr>
</tbody>
</table>
</div>
<p class="text-[11px] text-[var(--text-muted)]">
Showing {{ previewRows.length }} mock customers. Scores computed from current rules.
Preview will show customer scores and tiers once API is connected.
</p>
</div>
</div>