big refactor
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user