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('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) { 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) { 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, } }