WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

View File

@@ -0,0 +1,228 @@
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,
}
}