big refactor
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
usePageTitle('Agents & Commissions · Settings')
|
||||
|
||||
/* ── Types ── */
|
||||
type AgentStatus = 'active' | 'inactive' | 'suspended'
|
||||
type CommissionTier = { lobId: string; lob: string; newPct: number; renewalPct: number }
|
||||
|
||||
interface CommissionTier {
|
||||
lobId: string
|
||||
lob: string
|
||||
newPct: number
|
||||
renewalPct: number
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
@@ -20,83 +25,13 @@ interface Agent {
|
||||
lastLogin: string
|
||||
}
|
||||
|
||||
/* ── Mock data ── */
|
||||
const agents = ref<Agent[]>([
|
||||
{
|
||||
id: 'AG-001', name: 'Ana Ramírez', email: 'ana.ramirez@segur-os.com', phone: '+506 8812-4455',
|
||||
role: 'Senior producer', status: 'active', hireDate: '2021-03-15',
|
||||
book: { policies: 142, gwp: 2_840_000, collected: 2_610_000, outstanding: 230_000 },
|
||||
commissionTiers: [
|
||||
{ lobId: 'auto', lob: 'Auto', newPct: 15, renewalPct: 10 },
|
||||
{ lobId: 'health', lob: 'Health', newPct: 12, renewalPct: 8 },
|
||||
{ lobId: 'life', lob: 'Life', newPct: 18, renewalPct: 12 },
|
||||
{ lobId: 'property', lob: 'Property', newPct: 14, renewalPct: 9 },
|
||||
{ lobId: 'general', lob: 'General risk', newPct: 13, renewalPct: 8 },
|
||||
],
|
||||
ytdEarned: 48_200, ytdPending: 6_400, lastLogin: '2026-04-05 08:32'
|
||||
},
|
||||
{
|
||||
id: 'AG-002', name: 'Marco Villanueva', email: 'marco.v@segur-os.com', phone: '+506 8899-2211',
|
||||
role: 'Producer', status: 'active', hireDate: '2022-08-01',
|
||||
book: { policies: 88, gwp: 1_620_000, collected: 1_480_000, outstanding: 140_000 },
|
||||
commissionTiers: [
|
||||
{ lobId: 'auto', lob: 'Auto', newPct: 14, renewalPct: 9 },
|
||||
{ lobId: 'health', lob: 'Health', newPct: 11, renewalPct: 7 },
|
||||
{ lobId: 'life', lob: 'Life', newPct: 16, renewalPct: 10 },
|
||||
{ lobId: 'property', lob: 'Property', newPct: 12, renewalPct: 8 },
|
||||
{ lobId: 'general', lob: 'General risk', newPct: 12, renewalPct: 7 },
|
||||
],
|
||||
ytdEarned: 28_600, ytdPending: 3_200, lastLogin: '2026-04-04 17:15'
|
||||
},
|
||||
{
|
||||
id: 'AG-003', name: 'Lucía Fernández', email: 'lucia.f@segur-os.com', phone: '+506 7745-3388',
|
||||
role: 'Junior producer', status: 'active', hireDate: '2024-01-10',
|
||||
book: { policies: 34, gwp: 420_000, collected: 385_000, outstanding: 35_000 },
|
||||
commissionTiers: [
|
||||
{ lobId: 'auto', lob: 'Auto', newPct: 12, renewalPct: 8 },
|
||||
{ lobId: 'health', lob: 'Health', newPct: 10, renewalPct: 6 },
|
||||
{ lobId: 'life', lob: 'Life', newPct: 14, renewalPct: 9 },
|
||||
{ lobId: 'property', lob: 'Property', newPct: 11, renewalPct: 7 },
|
||||
{ lobId: 'general', lob: 'General risk', newPct: 10, renewalPct: 6 },
|
||||
],
|
||||
ytdEarned: 8_400, ytdPending: 1_100, lastLogin: '2026-04-05 09:01'
|
||||
},
|
||||
{
|
||||
id: 'AG-004', name: 'Diego Mora', email: 'diego.m@segur-os.com', phone: '+506 6612-9944',
|
||||
role: 'Producer', status: 'suspended', hireDate: '2023-05-20',
|
||||
book: { policies: 56, gwp: 980_000, collected: 720_000, outstanding: 260_000 },
|
||||
commissionTiers: [
|
||||
{ lobId: 'auto', lob: 'Auto', newPct: 13, renewalPct: 9 },
|
||||
{ lobId: 'health', lob: 'Health', newPct: 11, renewalPct: 7 },
|
||||
{ lobId: 'life', lob: 'Life', newPct: 15, renewalPct: 10 },
|
||||
{ lobId: 'property', lob: 'Property', newPct: 12, renewalPct: 8 },
|
||||
{ lobId: 'general', lob: 'General risk', newPct: 11, renewalPct: 7 },
|
||||
],
|
||||
ytdEarned: 14_200, ytdPending: 8_800, lastLogin: '2026-03-28 11:40'
|
||||
},
|
||||
{
|
||||
id: 'AG-005', name: 'Valentina Castro', email: 'val.castro@segur-os.com', phone: '+506 8834-5566',
|
||||
role: 'Senior producer', status: 'inactive', hireDate: '2019-11-03',
|
||||
book: { policies: 0, gwp: 0, collected: 0, outstanding: 0 },
|
||||
commissionTiers: [
|
||||
{ lobId: 'auto', lob: 'Auto', newPct: 15, renewalPct: 10 },
|
||||
{ lobId: 'health', lob: 'Health', newPct: 12, renewalPct: 8 },
|
||||
{ lobId: 'life', lob: 'Life', newPct: 18, renewalPct: 12 },
|
||||
{ lobId: 'property', lob: 'Property', newPct: 14, renewalPct: 9 },
|
||||
{ lobId: 'general', lob: 'General risk', newPct: 13, renewalPct: 8 },
|
||||
],
|
||||
ytdEarned: 0, ytdPending: 0, lastLogin: '2025-12-15 14:22'
|
||||
},
|
||||
])
|
||||
|
||||
/* ── State ── */
|
||||
const agents = ref<Agent[]>([])
|
||||
const search = ref('')
|
||||
const statusFilter = ref<'all' | AgentStatus>('all')
|
||||
const selectedAgent = ref<Agent | null>(null)
|
||||
const addModalOpen = ref(false)
|
||||
const editingCommissions = ref(false)
|
||||
|
||||
/* ── Filtering ── */
|
||||
const filteredAgents = computed(() => {
|
||||
let list = agents.value
|
||||
if (statusFilter.value !== 'all') list = list.filter(a => a.status === statusFilter.value)
|
||||
@@ -110,7 +45,6 @@ const filteredAgents = computed(() => {
|
||||
return list
|
||||
})
|
||||
|
||||
/* ── Aggregate KPIs ── */
|
||||
const kpis = computed(() => {
|
||||
const active = agents.value.filter(a => a.status === 'active')
|
||||
const totalBook = agents.value.reduce((s, a) => s + a.book.gwp, 0)
|
||||
@@ -130,7 +64,6 @@ const kpis = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/* ── Add agent modal ── */
|
||||
const newAgent = reactive({
|
||||
name: '', email: '', phone: '', role: 'Producer',
|
||||
defaultNewPct: 13, defaultRenewalPct: 8,
|
||||
@@ -177,11 +110,9 @@ function toggleAgentStatus(agent: Agent) {
|
||||
}
|
||||
|
||||
function resetCredentials(_agent: Agent) {
|
||||
// Mock: in production this would trigger a password reset email
|
||||
alert(`Password reset email would be sent to ${_agent.email}`)
|
||||
}
|
||||
|
||||
/* ── Helpers ── */
|
||||
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`
|
||||
|
||||
@@ -1,13 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertRecipient, CustomAlertRule } from '~/composables/useAlertConfig'
|
||||
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Alerts & Notifications · Settings')
|
||||
|
||||
const { config, addThreshold, removeThreshold, addPaymentTier, removePaymentTier, addCustomRule, updateCustomRule, removeCustomRule } = useAlertConfig()
|
||||
const toast = useToast()
|
||||
|
||||
// ── Recipient helpers ──────────────────────────────────────────────────────
|
||||
type AlertRecipient = 'handler' | 'manager' | 'customer' | 'custom'
|
||||
|
||||
interface Threshold {
|
||||
id: string
|
||||
daysBefore: number
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface PaymentTier {
|
||||
id: string
|
||||
daysOverdue: number
|
||||
action: string
|
||||
recipients: AlertRecipient[]
|
||||
}
|
||||
|
||||
interface CustomAlertRule {
|
||||
id: string
|
||||
alertName: string
|
||||
field: string
|
||||
operator: string
|
||||
value: number
|
||||
recipients: AlertRecipient[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface AlertConfig {
|
||||
emailSender: {
|
||||
senderEmail: string
|
||||
senderDisplayName: string
|
||||
replyToEmail: string
|
||||
}
|
||||
renewals: {
|
||||
enabled: boolean
|
||||
thresholds: Threshold[]
|
||||
}
|
||||
cancellations: {
|
||||
enabled: boolean
|
||||
recipients: AlertRecipient[]
|
||||
}
|
||||
latePayments: {
|
||||
enabled: boolean
|
||||
tiers: PaymentTier[]
|
||||
}
|
||||
creditCardExpiry: {
|
||||
enabled: boolean
|
||||
autoDebitOnly: boolean
|
||||
thresholds: Threshold[]
|
||||
}
|
||||
customRules: CustomAlertRule[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'policy-ui.alerts'
|
||||
|
||||
function loadConfig(): AlertConfig {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return defaultConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultConfig()
|
||||
}
|
||||
|
||||
function defaultConfig(): AlertConfig {
|
||||
return {
|
||||
emailSender: {
|
||||
senderEmail: '',
|
||||
senderDisplayName: '',
|
||||
replyToEmail: ''
|
||||
},
|
||||
renewals: {
|
||||
enabled: true,
|
||||
thresholds: [
|
||||
{ id: '1', daysBefore: 30, enabled: true },
|
||||
{ id: '2', daysBefore: 7, enabled: true }
|
||||
]
|
||||
},
|
||||
cancellations: {
|
||||
enabled: true,
|
||||
recipients: ['handler', 'manager']
|
||||
},
|
||||
latePayments: {
|
||||
enabled: true,
|
||||
tiers: [
|
||||
{ id: '1', daysOverdue: 15, action: 'First reminder', recipients: ['handler'] },
|
||||
{ id: '2', daysOverdue: 30, action: 'Second reminder', recipients: ['handler', 'manager'] },
|
||||
{ id: '3', daysOverdue: 45, action: 'Escalate to collections', recipients: ['handler', 'manager', 'customer'] }
|
||||
]
|
||||
},
|
||||
creditCardExpiry: {
|
||||
enabled: true,
|
||||
autoDebitOnly: true,
|
||||
thresholds: [
|
||||
{ id: '1', daysBefore: 30, enabled: true },
|
||||
{ id: '2', daysBefore: 7, enabled: true }
|
||||
]
|
||||
},
|
||||
customRules: []
|
||||
}
|
||||
}
|
||||
|
||||
const config = ref<AlertConfig>(loadConfig())
|
||||
|
||||
function saveConfig() {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config.value))
|
||||
}
|
||||
}
|
||||
|
||||
function addThreshold(type: 'renewals' | 'creditCardExpiry', days: number) {
|
||||
const id = String(Date.now())
|
||||
config.value[type].thresholds.push({ id, daysBefore: days, enabled: true })
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
function removeThreshold(type: 'renewals' | 'creditCardExpiry', id: string) {
|
||||
config.value[type].thresholds = config.value[type].thresholds.filter(t => t.id !== id)
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
function addPaymentTier(days: number, action: string, recipients: AlertRecipient[]) {
|
||||
const id = String(Date.now())
|
||||
config.value.latePayments.tiers.push({ id, daysOverdue: days, action, recipients: [...recipients] })
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
function removePaymentTier(id: string) {
|
||||
config.value.latePayments.tiers = config.value.latePayments.tiers.filter(t => t.id !== id)
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
function addCustomRule(rule: Omit<CustomAlertRule, 'id'>) {
|
||||
const id = String(Date.now())
|
||||
config.value.customRules.push({ ...rule, id })
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
function updateCustomRule(id: string, updates: Partial<CustomAlertRule>) {
|
||||
const idx = config.value.customRules.findIndex(r => r.id === id)
|
||||
if (idx !== -1) {
|
||||
config.value.customRules[idx] = { ...config.value.customRules[idx], ...updates }
|
||||
saveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function removeCustomRule(id: string) {
|
||||
config.value.customRules = config.value.customRules.filter(r => r.id !== id)
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
const RECIPIENT_OPTIONS: { id: AlertRecipient; label: string }[] = [
|
||||
{ id: 'handler', label: 'Handler' },
|
||||
{ id: 'manager', label: 'Manager' },
|
||||
@@ -19,9 +169,9 @@ function toggleRecipient(list: AlertRecipient[], r: AlertRecipient) {
|
||||
const idx = list.indexOf(r)
|
||||
if (idx === -1) list.push(r)
|
||||
else list.splice(idx, 1)
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// ── Threshold add ──────────────────────────────────────────────────────────
|
||||
const newRenewalDays = ref(7)
|
||||
const newCcDays = ref(7)
|
||||
|
||||
@@ -29,19 +179,18 @@ function addRenewalThreshold() {
|
||||
addThreshold('renewals', newRenewalDays.value)
|
||||
newRenewalDays.value = 7
|
||||
}
|
||||
|
||||
function addCcThreshold() {
|
||||
addThreshold('creditCardExpiry', newCcDays.value)
|
||||
newCcDays.value = 7
|
||||
}
|
||||
|
||||
// ── Late payment tier add ──────────────────────────────────────────────────
|
||||
const newLpDays = ref(45)
|
||||
function addNewPaymentTier() {
|
||||
addPaymentTier(newLpDays.value, 'Notify assigned handler', ['handler'])
|
||||
newLpDays.value = 45
|
||||
}
|
||||
|
||||
// ── Custom rule add ────────────────────────────────────────────────────────
|
||||
const FIELD_OPTIONS = [
|
||||
{ value: 'premium', label: 'Annual premium' },
|
||||
{ value: 'policy_count', label: 'Policy count' },
|
||||
@@ -75,7 +224,6 @@ function submitNewRule() {
|
||||
showNewRule.value = false
|
||||
}
|
||||
|
||||
// ── Escalation dot color ───────────────────────────────────────────────────
|
||||
function tierDotClass(idx: number, total: number) {
|
||||
if (total <= 1) return 'al-dot-green'
|
||||
if (idx === total - 1) return 'al-dot-red'
|
||||
@@ -84,6 +232,7 @@ function tierDotClass(idx: number, total: number) {
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
saveConfig()
|
||||
toast.add({ title: 'Alert configuration saved', color: 'green' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '~/utils/refDebounced'
|
||||
import type { FormCatalogRow } from '~/types/form-catalog'
|
||||
import { productLineLabel, useFormsCatalog } from '~/composables/useFormsCatalog'
|
||||
|
||||
usePageTitle('Forms library · Settings')
|
||||
|
||||
const { rows, version, insurerItems, subRamoItems } = useFormsCatalog()
|
||||
interface FormCatalogRow {
|
||||
id: string
|
||||
description: string
|
||||
insurerSlugs: string[]
|
||||
subRamoLabel: string
|
||||
subRamoKey: string
|
||||
personKinds: 'natural' | 'juridica' | 'both'
|
||||
productLine: string | null
|
||||
fileLabel: string
|
||||
fileUrl: string
|
||||
badge: string | null
|
||||
}
|
||||
|
||||
const rows = ref<FormCatalogRow[]>([])
|
||||
const version = ref('1.0.0')
|
||||
const insurerItems = ref<{ label: string; value: string }[]>([])
|
||||
const subRamoItems = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
const pageSize = ref(10)
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const debouncedSearch = refDebounced(search, 250)
|
||||
|
||||
const pageSizeItems = [
|
||||
{ label: '10', value: 10 },
|
||||
@@ -32,12 +43,12 @@ function rowSearchText(r: FormCatalogRow): string {
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = debouncedSearch.value.trim().toLowerCase()
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (!q) return rows.value
|
||||
return rows.value.filter((r) => rowSearchText(r).includes(q))
|
||||
})
|
||||
|
||||
watch([debouncedSearch, pageSize], () => {
|
||||
watch([search, pageSize], () => {
|
||||
page.value = 1
|
||||
})
|
||||
|
||||
@@ -73,6 +84,11 @@ function personLabel(pk: FormCatalogRow['personKinds']) {
|
||||
return pk === 'natural' ? 'Natural' : 'Jurídica'
|
||||
}
|
||||
|
||||
function productLineLabel(pl: string | null) {
|
||||
if (!pl) return '—'
|
||||
return pl
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
const headers = [
|
||||
'ID',
|
||||
|
||||
@@ -1,8 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
usePageTitle('Settings')
|
||||
|
||||
const { isSuperAdmin } = useSuperAdmin()
|
||||
const sidebarFeatures = useSidebarFeatures()
|
||||
const isSuperAdmin = computed(() => {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem('policy-ui.superadmin')
|
||||
return stored !== '0'
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const sidebarFeatures = reactive({
|
||||
showWorkstations: computed({
|
||||
get: () => {
|
||||
if (import.meta.client) {
|
||||
return localStorage.getItem('policy-ui.sidebar.workstations') !== 'false'
|
||||
}
|
||||
return true
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('policy-ui.sidebar.workstations', String(value))
|
||||
}
|
||||
}
|
||||
}),
|
||||
showAiTools: computed({
|
||||
get: () => {
|
||||
if (import.meta.client) {
|
||||
return localStorage.getItem('policy-ui.sidebar.ai-tools') !== 'false'
|
||||
}
|
||||
return true
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('policy-ui.sidebar.ai-tools', String(value))
|
||||
}
|
||||
}
|
||||
}),
|
||||
showLeadsHub: computed({
|
||||
get: () => {
|
||||
if (import.meta.client) {
|
||||
return localStorage.getItem('policy-ui.sidebar.leads-hub') !== 'false'
|
||||
}
|
||||
return true
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('policy-ui.sidebar.leads-hub', String(value))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const cards = computed(() => {
|
||||
const base: { to: string; title: string; description: string; icon: string }[] = []
|
||||
|
||||
@@ -1,26 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { MAX_LOGO_FILE_BYTES } from '~/types/branding'
|
||||
import type { BrokerageBrandingState } from '~/types/branding'
|
||||
|
||||
definePageMeta({ ssr: false })
|
||||
|
||||
usePageTitle('Organization · Settings')
|
||||
|
||||
const { isSuperAdmin } = useSuperAdmin()
|
||||
const toast = useToast()
|
||||
const { saved, defaultBrokerageBranding } = useBrokerageBranding()
|
||||
const draft = ref<BrokerageBrandingState>(defaultBrokerageBranding())
|
||||
|
||||
const MAX_LOGO_FILE_BYTES = 512 * 1024
|
||||
|
||||
interface BrokerageBrandingState {
|
||||
companyName: string
|
||||
logoDataUrl: string | null
|
||||
logoFileName: string
|
||||
reportPageHeader: string
|
||||
reportPageFooter: string
|
||||
}
|
||||
|
||||
const isSuperAdmin = computed(() => {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem('policy-ui.superadmin')
|
||||
return stored !== '0'
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const STORAGE_KEY = 'policy-ui.branding'
|
||||
|
||||
function loadBranding(): BrokerageBrandingState {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return defaultBranding()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultBranding()
|
||||
}
|
||||
|
||||
function defaultBranding(): BrokerageBrandingState {
|
||||
return {
|
||||
companyName: '',
|
||||
logoDataUrl: null,
|
||||
logoFileName: '',
|
||||
reportPageHeader: '',
|
||||
reportPageFooter: ''
|
||||
}
|
||||
}
|
||||
|
||||
const saved = ref<BrokerageBrandingState>(loadBranding())
|
||||
const draft = ref<BrokerageBrandingState>({ ...saved.value })
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function syncDraftFromSaved() {
|
||||
const s = saved.value
|
||||
draft.value = {
|
||||
companyName: s.companyName,
|
||||
logoDataUrl: s.logoDataUrl,
|
||||
logoFileName: s.logoFileName,
|
||||
reportPageHeader: s.reportPageHeader,
|
||||
reportPageFooter: s.reportPageFooter
|
||||
}
|
||||
draft.value = { ...saved.value }
|
||||
}
|
||||
|
||||
onMounted(() => syncDraftFromSaved())
|
||||
@@ -65,6 +99,9 @@ function clearLogo() {
|
||||
|
||||
function save() {
|
||||
saved.value = { ...draft.value }
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved.value))
|
||||
}
|
||||
toast.add({
|
||||
title: 'Organization saved',
|
||||
description: 'Sidebar and home use this brokerage identity for all users.',
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '~/utils/refDebounced'
|
||||
import { ROLES_SEGUROS_SEED, SEGUROS_PERMISSION_COLUMNS } from '~/data/roles-seguros'
|
||||
import type { RoleRow } from '~/types/roles'
|
||||
|
||||
usePageTitle('Permissions · Settings')
|
||||
|
||||
interface RoleRow {
|
||||
id: string
|
||||
description: string
|
||||
active: boolean
|
||||
seguros: Record<string, boolean>
|
||||
}
|
||||
|
||||
const pageSize = ref(10)
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const debouncedSearch = refDebounced(search, 250)
|
||||
|
||||
const pageSizeItems = [
|
||||
{ label: '10', value: 10 },
|
||||
@@ -16,17 +18,27 @@ const pageSizeItems = [
|
||||
{ label: '50', value: 50 }
|
||||
]
|
||||
|
||||
const rows = ref<RoleRow[]>([...ROLES_SEGUROS_SEED])
|
||||
const SEGUROS_PERMISSION_COLUMNS = [
|
||||
{ key: 'quotes', label: 'Quotes', icon: 'i-heroicons-document-text' },
|
||||
{ key: 'policies', label: 'Policies', icon: 'i-heroicons-shield-check' },
|
||||
{ key: 'claims', label: 'Claims', icon: 'i-heroicons-exclamation-triangle' },
|
||||
{ key: 'billing', label: 'Billing', icon: 'i-heroicons-credit-card' },
|
||||
{ key: 'customers', label: 'Customers', icon: 'i-heroicons-users' },
|
||||
{ key: 'providers', label: 'Providers', icon: 'i-heroicons-building-office-2' },
|
||||
{ key: 'reports', label: 'Reports', icon: 'i-heroicons-chart-bar' },
|
||||
]
|
||||
|
||||
const rows = ref<RoleRow[]>([])
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = debouncedSearch.value.trim().toLowerCase()
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (!q) return rows.value
|
||||
return rows.value.filter(
|
||||
(r) => String(r.id).includes(q) || r.description.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
watch([debouncedSearch, pageSize], () => {
|
||||
watch([search, pageSize], () => {
|
||||
page.value = 1
|
||||
})
|
||||
|
||||
|
||||
@@ -1,35 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { useProfileLayouts } from '~/composables/useProfileLayouts'
|
||||
import type { ProfileLayout } from '~/composables/useProfileLayouts'
|
||||
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Profile Layouts · Settings')
|
||||
|
||||
const {
|
||||
layouts,
|
||||
activeLayoutId,
|
||||
activeLayout,
|
||||
sortedSections,
|
||||
setActiveLayout,
|
||||
updateLayout,
|
||||
removeCustomLayout,
|
||||
resetToDefaults,
|
||||
} = useProfileLayouts()
|
||||
interface ProfileSection {
|
||||
id: string
|
||||
label: string
|
||||
visible: boolean
|
||||
order: number
|
||||
}
|
||||
|
||||
interface ProfileLayout {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
isCustom: boolean
|
||||
defaultTab: string
|
||||
sections: ProfileSection[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'policy-ui.profile-layouts'
|
||||
|
||||
function loadLayouts(): ProfileLayout[] {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return defaultLayouts()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultLayouts()
|
||||
}
|
||||
|
||||
function defaultLayouts(): ProfileLayout[] {
|
||||
return [
|
||||
{
|
||||
id: 'agent',
|
||||
name: 'Agent',
|
||||
description: 'For producers and account executives',
|
||||
icon: 'i-heroicons-user',
|
||||
isCustom: false,
|
||||
defaultTab: 'policies',
|
||||
sections: [
|
||||
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
|
||||
{ id: 'policies', label: 'Policies', visible: true, order: 2 },
|
||||
{ id: 'quotes', label: 'Quotes', visible: true, order: 3 },
|
||||
{ id: 'claims', label: 'Claims', visible: true, order: 4 },
|
||||
{ id: 'billing', label: 'Billing', visible: true, order: 5 },
|
||||
{ id: 'documents', label: 'Documents', visible: true, order: 6 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'manager',
|
||||
name: 'Manager',
|
||||
description: 'For team leads and supervisors',
|
||||
icon: 'i-heroicons-users',
|
||||
isCustom: false,
|
||||
defaultTab: 'team',
|
||||
sections: [
|
||||
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
|
||||
{ id: 'team', label: 'Team', visible: true, order: 2 },
|
||||
{ id: 'performance', label: 'Performance', visible: true, order: 3 },
|
||||
{ id: 'policies', label: 'Policies', visible: true, order: 4 },
|
||||
{ id: 'reports', label: 'Reports', visible: true, order: 5 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
description: 'For administrators and operations',
|
||||
icon: 'i-heroicons-shield-check',
|
||||
isCustom: false,
|
||||
defaultTab: 'overview',
|
||||
sections: [
|
||||
{ id: 'overview', label: 'Overview', visible: true, order: 1 },
|
||||
{ id: 'users', label: 'Users', visible: true, order: 2 },
|
||||
{ id: 'settings', label: 'Settings', visible: true, order: 3 },
|
||||
{ id: 'audit', label: 'Audit Log', visible: true, order: 4 },
|
||||
{ id: 'integrations', label: 'Integrations', visible: true, order: 5 },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const layouts = ref<ProfileLayout[]>(loadLayouts())
|
||||
const activeLayoutId = ref('agent')
|
||||
|
||||
function saveLayouts() {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts.value))
|
||||
}
|
||||
}
|
||||
|
||||
const activeLayout = computed(() => layouts.value.find(l => l.id === activeLayoutId.value) || layouts.value[0])
|
||||
|
||||
const sortedSections = computed(() => {
|
||||
if (!activeLayout.value) return []
|
||||
return [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
function setActiveLayout(id: string) {
|
||||
activeLayoutId.value = id
|
||||
}
|
||||
|
||||
function updateLayout(id: string, updates: Partial<ProfileLayout>) {
|
||||
const idx = layouts.value.findIndex(l => l.id === id)
|
||||
if (idx !== -1) {
|
||||
layouts.value[idx] = { ...layouts.value[idx], ...updates }
|
||||
saveLayouts()
|
||||
}
|
||||
}
|
||||
|
||||
function removeCustomLayout(id: string) {
|
||||
layouts.value = layouts.value.filter(l => l.id !== id)
|
||||
saveLayouts()
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
layouts.value = defaultLayouts()
|
||||
activeLayoutId.value = 'agent'
|
||||
saveLayouts()
|
||||
}
|
||||
|
||||
/* ── Built-in vs custom ── */
|
||||
const builtInLayouts = computed(() => layouts.value.filter(l => !l.isCustom))
|
||||
const customLayouts = computed(() => layouts.value.filter(l => l.isCustom))
|
||||
|
||||
/* ── Toggle section visibility ── */
|
||||
function toggleSectionVisibility(sectionId: string) {
|
||||
if (!activeLayout.value) return
|
||||
const updated = activeLayout.value.sections.map(s =>
|
||||
s.id === sectionId ? { ...s, visible: !s.visible } : s
|
||||
)
|
||||
updateLayout(activeLayout.value.id, { sections: updated })
|
||||
}
|
||||
|
||||
/* ── Move section up/down ── */
|
||||
function moveSection(sectionId: string, direction: 'up' | 'down') {
|
||||
if (!activeLayout.value) return
|
||||
const sections = [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
const idx = sections.findIndex(s => s.id === sectionId)
|
||||
if (idx < 0) return
|
||||
@@ -43,12 +151,11 @@ function moveSection(sectionId: string, direction: 'up' | 'down') {
|
||||
updateLayout(activeLayout.value.id, { sections })
|
||||
}
|
||||
|
||||
/* ── Ordered sections for display ── */
|
||||
const orderedSections = computed(() =>
|
||||
[...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
)
|
||||
const orderedSections = computed(() => {
|
||||
if (!activeLayout.value) return []
|
||||
return [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
/* ── Delete custom layout ── */
|
||||
function handleDelete(id: string) {
|
||||
removeCustomLayout(id)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
usePageTitle('Quote requests · Settings')
|
||||
|
||||
const { quoteRequestEmailEnabled, setQuoteRequestEmailEnabled } = useQuoteRequestEmailEnabled()
|
||||
const STORAGE_KEY = 'policy-ui.quote-request-email-enabled'
|
||||
|
||||
const quoteRequestEmailEnabled = computed({
|
||||
get: () => {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
return stored !== 'false'
|
||||
}
|
||||
return true
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, String(value))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function setQuoteRequestEmailEnabled(value: boolean) {
|
||||
quoteRequestEmailEnabled.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,12 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useReferralChannels, type ReferralChannel } from '~/composables/useReferralChannels'
|
||||
|
||||
usePageTitle('Referral Channels · Settings')
|
||||
|
||||
const { channels, addChannel, updateChannel, removeChannel } = useReferralChannels()
|
||||
interface ReferralChannel {
|
||||
id: string
|
||||
name: string
|
||||
type: 'person' | 'company' | 'digital' | 'event' | 'other'
|
||||
contactName: string
|
||||
contactPhone: string
|
||||
contactEmail: string
|
||||
note: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'policy-ui.referral-channels'
|
||||
|
||||
function loadChannels(): ReferralChannel[] {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const channels = ref<ReferralChannel[]>(loadChannels())
|
||||
|
||||
function saveChannels() {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(channels.value))
|
||||
}
|
||||
}
|
||||
|
||||
function addChannel(channel: Omit<ReferralChannel, 'id'>) {
|
||||
const id = String(Date.now())
|
||||
channels.value.push({ ...channel, id })
|
||||
saveChannels()
|
||||
}
|
||||
|
||||
function updateChannel(id: string, updates: Partial<ReferralChannel>) {
|
||||
const idx = channels.value.findIndex(c => c.id === id)
|
||||
if (idx !== -1) {
|
||||
channels.value[idx] = { ...channels.value[idx], ...updates }
|
||||
saveChannels()
|
||||
}
|
||||
}
|
||||
|
||||
function removeChannel(id: string) {
|
||||
channels.value = channels.value.filter(c => c.id !== id)
|
||||
saveChannels()
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
/* ── Form state ── */
|
||||
const formOpen = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
@@ -88,7 +138,6 @@ function toggleActive(id: string) {
|
||||
if (ch) updateChannel(id, { active: !ch.active })
|
||||
}
|
||||
|
||||
/* ── Filter ── */
|
||||
type ListFilter = 'all' | 'active' | 'inactive'
|
||||
const activeFilter = ref<ListFilter>('all')
|
||||
|
||||
@@ -104,7 +153,6 @@ const filterCounts = computed(() => ({
|
||||
inactive: channels.value.filter(c => !c.active).length,
|
||||
}))
|
||||
|
||||
/* ── Helpers ── */
|
||||
const typeMeta: Record<string, { label: string; icon: string; class: string }> = {
|
||||
person: { label: 'Person', icon: 'i-heroicons-user', class: 'rc-type-person' },
|
||||
company: { label: 'Company', icon: 'i-heroicons-building-office', class: 'rc-type-company' },
|
||||
|
||||
@@ -1,9 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { TIER_LABELS, QUEUE_LABELS, type RoutingTier, type RoutedQueue, type RoutingRule } from '~/data/mock-support'
|
||||
|
||||
usePageTitle('Support Routing')
|
||||
|
||||
const { state, toggleRule, updateRule } = useSupportTickets()
|
||||
type RoutingTier = 'tier1_auto' | 'tier2_rule' | 'tier3_open'
|
||||
type RoutedQueue = 'collections' | 'claims' | 'sales' | 'renewals' | 'operations' | 'open_pool'
|
||||
|
||||
interface RoutingRule {
|
||||
id: string
|
||||
name: string
|
||||
condition: string
|
||||
tier: RoutingTier
|
||||
targetQueue: RoutedQueue
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface SupportState {
|
||||
routingRules: RoutingRule[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'policy-ui.support-routing'
|
||||
|
||||
function loadState(): SupportState {
|
||||
if (import.meta.client) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return defaultState()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultState()
|
||||
}
|
||||
|
||||
function defaultState(): SupportState {
|
||||
return {
|
||||
routingRules: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Existing customer with LOB',
|
||||
condition: 'Customer exists and has LOB in message',
|
||||
tier: 'tier1_auto',
|
||||
targetQueue: 'operations',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Web form submission',
|
||||
condition: 'Form submission with customer data',
|
||||
tier: 'tier1_auto',
|
||||
targetQueue: 'sales',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Payment inquiry',
|
||||
condition: 'Keywords: pago, factura, cobro, payment',
|
||||
tier: 'tier2_rule',
|
||||
targetQueue: 'collections',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Claim inquiry',
|
||||
condition: 'Keywords: siniestro, reclamo, claim, accident',
|
||||
tier: 'tier2_rule',
|
||||
targetQueue: 'claims',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Renewal inquiry',
|
||||
condition: 'Keywords: renovación, renewal, vence',
|
||||
tier: 'tier2_rule',
|
||||
targetQueue: 'renewals',
|
||||
enabled: true
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const state = ref<SupportState>(loadState())
|
||||
|
||||
function saveState() {
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.value))
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRule(id: string) {
|
||||
const rule = state.value.routingRules.find(r => r.id === id)
|
||||
if (rule) {
|
||||
rule.enabled = !rule.enabled
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
function updateRule(id: string, updates: Partial<RoutingRule>) {
|
||||
const idx = state.value.routingRules.findIndex(r => r.id === id)
|
||||
if (idx !== -1) {
|
||||
state.value.routingRules[idx] = { ...state.value.routingRules[idx], ...updates }
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
type TierTab = 'tier1' | 'tier2' | 'tier3'
|
||||
const activeTier = ref<TierTab>('tier1')
|
||||
@@ -26,12 +125,21 @@ const queueOptions: { label: string; value: RoutedQueue }[] = [
|
||||
{ label: 'Pool Abierto', value: 'open_pool' },
|
||||
]
|
||||
|
||||
// Tier 3 settings (mock)
|
||||
const QUEUE_LABELS: Record<RoutedQueue, string> = {
|
||||
collections: 'Cobros',
|
||||
claims: 'Siniestros',
|
||||
sales: 'Ventas',
|
||||
renewals: 'Renovaciones',
|
||||
operations: 'Operaciones',
|
||||
open_pool: 'Pool Abierto',
|
||||
}
|
||||
|
||||
const tier3DefaultAssignee = ref('Round-robin')
|
||||
const tier3EscalationHours = ref(4)
|
||||
|
||||
const toast = useToast()
|
||||
function handleSave() {
|
||||
saveState()
|
||||
toast.add({ title: 'Configuración guardada', color: 'green' })
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user