229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import { computed } from 'vue'
|
|
import { useLocalStorageRef } from '~/utils/useLocalStorageRef'
|
|
|
|
/* ── Types ── */
|
|
|
|
export type ServiceTierId = string
|
|
|
|
export interface ServiceTier {
|
|
id: ServiceTierId
|
|
name: string
|
|
color: string
|
|
icon: string
|
|
description: string
|
|
minScore: number
|
|
benefits: string[]
|
|
}
|
|
|
|
export interface AttentionRule {
|
|
id: string
|
|
field: 'premium' | 'policy_count' | 'commission' | 'collectivo_member' | 'multi_line' | 'tenure_years' | 'has_private_policies'
|
|
operator: 'gte' | 'lte' | 'eq' | 'gt' | 'lt'
|
|
value: number | boolean
|
|
points: number
|
|
label: string
|
|
}
|
|
|
|
export interface CustomerAttentionConfig {
|
|
tiers: ServiceTier[]
|
|
rules: AttentionRule[]
|
|
autoClassify: boolean
|
|
}
|
|
|
|
/* ── Defaults ── */
|
|
|
|
function defaultTiers(): ServiceTier[] {
|
|
return [
|
|
{
|
|
id: 'platinum',
|
|
name: 'Platinum',
|
|
color: '#7c3aed',
|
|
icon: 'i-heroicons-star',
|
|
description: 'VIP multi-line clients with highest lifetime value',
|
|
minScore: 80,
|
|
benefits: ['Priority claims handling', 'Dedicated account manager', 'Annual review', 'Renewal negotiation priority'],
|
|
},
|
|
{
|
|
id: 'gold',
|
|
name: 'Gold',
|
|
color: '#d4a017',
|
|
icon: 'i-heroicons-trophy',
|
|
description: 'Established clients with strong portfolio',
|
|
minScore: 55,
|
|
benefits: ['Priority support', 'Proactive renewal outreach', 'Cross-sell consultation'],
|
|
},
|
|
{
|
|
id: 'silver',
|
|
name: 'Silver',
|
|
color: '#6b7280',
|
|
icon: 'i-heroicons-shield-check',
|
|
description: 'Active clients with growth potential',
|
|
minScore: 30,
|
|
benefits: ['Standard support', 'Regular check-ins'],
|
|
},
|
|
{
|
|
id: 'standard',
|
|
name: 'Standard',
|
|
color: '#01696f',
|
|
icon: 'i-heroicons-user',
|
|
description: 'All active customers',
|
|
minScore: 0,
|
|
benefits: ['Standard service'],
|
|
},
|
|
]
|
|
}
|
|
|
|
function defaultRules(): AttentionRule[] {
|
|
return [
|
|
{ id: 'r1', field: 'premium', operator: 'gte', value: 5000, points: 25, label: 'Annual premium >= $5,000' },
|
|
{ id: 'r2', field: 'premium', operator: 'gte', value: 10000, points: 40, label: 'Annual premium >= $10,000' },
|
|
{ id: 'r3', field: 'policy_count', operator: 'gte', value: 3, points: 15, label: '3+ active policies' },
|
|
{ id: 'r4', field: 'policy_count', operator: 'gte', value: 5, points: 25, label: '5+ active policies' },
|
|
{ id: 'r5', field: 'multi_line', operator: 'gte', value: 3, points: 20, label: 'Multi-line (3+ different lines)' },
|
|
{ id: 'r6', field: 'tenure_years', operator: 'gte', value: 3, points: 10, label: 'Client for 3+ years' },
|
|
{ id: 'r7', field: 'tenure_years', operator: 'gte', value: 5, points: 20, label: 'Client for 5+ years' },
|
|
{ id: 'r8', field: 'collectivo_member', operator: 'eq', value: true, points: 15, label: 'Collectivo member with private policies' },
|
|
{ id: 'r9', field: 'commission', operator: 'gte', value: 1000, points: 10, label: 'Annual commission >= $1,000' },
|
|
]
|
|
}
|
|
|
|
function defaultConfig(): CustomerAttentionConfig {
|
|
return {
|
|
tiers: defaultTiers(),
|
|
rules: defaultRules(),
|
|
autoClassify: true,
|
|
}
|
|
}
|
|
|
|
/* ── Customer input shape ── */
|
|
|
|
export interface CustomerAttentionInput {
|
|
totalPremium: number
|
|
policyCount: number
|
|
lineCount: number
|
|
tenureYears: number
|
|
isCollectivoMember: boolean
|
|
hasPrivatePolicies: boolean
|
|
estimatedCommission: number
|
|
}
|
|
|
|
/* ── Rule evaluation ── */
|
|
|
|
function evaluateRule(rule: AttentionRule, customer: CustomerAttentionInput): boolean {
|
|
let fieldValue: number | boolean
|
|
|
|
switch (rule.field) {
|
|
case 'premium':
|
|
fieldValue = customer.totalPremium
|
|
break
|
|
case 'policy_count':
|
|
fieldValue = customer.policyCount
|
|
break
|
|
case 'commission':
|
|
fieldValue = customer.estimatedCommission
|
|
break
|
|
case 'multi_line':
|
|
fieldValue = customer.lineCount
|
|
break
|
|
case 'tenure_years':
|
|
fieldValue = customer.tenureYears
|
|
break
|
|
case 'collectivo_member':
|
|
// Special: collectivo member rule only matches if also has private policies
|
|
return rule.value === true && customer.isCollectivoMember && customer.hasPrivatePolicies
|
|
case 'has_private_policies':
|
|
return typeof rule.value === 'boolean' ? customer.hasPrivatePolicies === rule.value : false
|
|
default:
|
|
return false
|
|
}
|
|
|
|
if (typeof fieldValue === 'boolean' || typeof rule.value === 'boolean') return false
|
|
|
|
switch (rule.operator) {
|
|
case 'gte': return (fieldValue as number) >= (rule.value as number)
|
|
case 'gt': return (fieldValue as number) > (rule.value as number)
|
|
case 'lte': return (fieldValue as number) <= (rule.value as number)
|
|
case 'lt': return (fieldValue as number) < (rule.value as number)
|
|
case 'eq': return (fieldValue as number) === (rule.value as number)
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
/* ── Composable ── */
|
|
|
|
export function useCustomerAttention() {
|
|
const config = useLocalStorageRef<CustomerAttentionConfig>('policy-ui-customer-attention-v1', defaultConfig)
|
|
|
|
const tiers = computed(() => [...config.value.tiers].sort((a, b) => b.minScore - a.minScore))
|
|
|
|
const rules = computed(() => config.value.rules)
|
|
|
|
function getScoreForCustomer(customer: CustomerAttentionInput): number {
|
|
let score = 0
|
|
for (const rule of config.value.rules) {
|
|
if (evaluateRule(rule, customer)) {
|
|
score += rule.points
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
function getTierForCustomer(customer: CustomerAttentionInput): ServiceTier {
|
|
const score = getScoreForCustomer(customer)
|
|
const sorted = [...config.value.tiers].sort((a, b) => b.minScore - a.minScore)
|
|
for (const tier of sorted) {
|
|
if (score >= tier.minScore) return tier
|
|
}
|
|
// Fallback to lowest tier
|
|
return sorted[sorted.length - 1] ?? config.value.tiers[0]
|
|
}
|
|
|
|
/* ── Tier CRUD ── */
|
|
|
|
function addTier(tier: ServiceTier) {
|
|
config.value.tiers.push(tier)
|
|
}
|
|
|
|
function updateTier(id: string, patch: Partial<ServiceTier>) {
|
|
const idx = config.value.tiers.findIndex(t => t.id === id)
|
|
if (idx !== -1) {
|
|
config.value.tiers[idx] = { ...config.value.tiers[idx], ...patch }
|
|
}
|
|
}
|
|
|
|
function removeTier(id: string) {
|
|
config.value.tiers = config.value.tiers.filter(t => t.id !== id)
|
|
}
|
|
|
|
/* ── Rule CRUD ── */
|
|
|
|
function addRule(rule: AttentionRule) {
|
|
config.value.rules.push(rule)
|
|
}
|
|
|
|
function updateRule(id: string, patch: Partial<AttentionRule>) {
|
|
const idx = config.value.rules.findIndex(r => r.id === id)
|
|
if (idx !== -1) {
|
|
config.value.rules[idx] = { ...config.value.rules[idx], ...patch }
|
|
}
|
|
}
|
|
|
|
function removeRule(id: string) {
|
|
config.value.rules = config.value.rules.filter(r => r.id !== id)
|
|
}
|
|
|
|
return {
|
|
config,
|
|
tiers,
|
|
rules,
|
|
getScoreForCustomer,
|
|
getTierForCustomer,
|
|
addTier,
|
|
updateTier,
|
|
removeTier,
|
|
addRule,
|
|
updateRule,
|
|
removeRule,
|
|
}
|
|
}
|