/** * Support Tickets — composable for queue state, filtering, CRUD, and mock routing. * Persisted in localStorage via useLocalStorageRef. */ import { useLocalStorageRef } from '~/utils/useLocalStorageRef' import { type SupportTicket, type SupportTicketDetail, type TicketMessage, type TicketStatus, type SupportChannel, type RoutingTier, type RoutedQueue, type RoutingRule, MOCK_SUPPORT_TICKETS, MOCK_TICKET_DETAILS, MOCK_ROUTING_RULES, } from '~/data/mock-support' interface SupportState { tickets: SupportTicket[] details: Record routingRules: RoutingRule[] } function buildDefaults(): SupportState { return { tickets: [...MOCK_SUPPORT_TICKETS], details: { ...MOCK_TICKET_DETAILS }, routingRules: [...MOCK_ROUTING_RULES], } } export function useSupportTickets() { const state = useLocalStorageRef('policy-ui-support-tickets-v1', buildDefaults) // ── Computed: filtered lists ── const openTickets = computed(() => state.value.tickets.filter(t => t.status === 'open')) const unresolvedCount = computed(() => state.value.tickets.filter(t => t.status !== 'resolved').length) const breachedCount = computed(() => state.value.tickets.filter(t => t.slaPercent >= 100).length) const unassignedCount = computed(() => state.value.tickets.filter(t => !t.assignedTo).length) const openPoolCount = computed(() => state.value.tickets.filter(t => t.routedQueue === 'open_pool').length) const inProgressCount = computed(() => state.value.tickets.filter(t => t.status === 'in_progress').length) const kpis = computed(() => { const all = state.value.tickets const unresolved = all.filter(t => t.status !== 'resolved') const avgDaysOpen = unresolved.length ? Math.round(unresolved.reduce((sum, t) => sum + t.daysOpen, 0) / unresolved.length) : 0 return { total: all.length, open: openTickets.value.length, inProgress: inProgressCount.value, breached: breachedCount.value, avgDaysOpen, unassigned: unassignedCount.value, openPool: openPoolCount.value, } }) // ── CRUD ── function updateStatus(ticketId: string, status: TicketStatus) { const ticket = state.value.tickets.find(t => t.id === ticketId) if (ticket) { ticket.status = status ticket.updatedAt = new Date().toISOString().slice(0, 10) } const detail = state.value.details[ticketId] if (detail) { detail.status = status detail.updatedAt = new Date().toISOString().slice(0, 10) } } function assignTicket(ticketId: string, agent: string) { const ticket = state.value.tickets.find(t => t.id === ticketId) if (ticket) { ticket.assignedTo = agent ticket.updatedAt = new Date().toISOString().slice(0, 10) } const detail = state.value.details[ticketId] if (detail) { detail.assignedTo = agent detail.updatedAt = new Date().toISOString().slice(0, 10) detail.messages.push({ id: `msg-${Date.now()}`, type: 'system', direction: 'internal', from: 'Sistema', to: null, subject: null, body: `Ticket asignado a ${agent}`, timestamp: new Date().toISOString(), aiDigest: null, }) } } function addMessage(ticketId: string, message: Omit) { const detail = state.value.details[ticketId] if (!detail) return const msg: TicketMessage = { ...message, id: `msg-${Date.now()}`, timestamp: new Date().toISOString(), } detail.messages.push(msg) detail.messageCount = detail.messages.length detail.updatedAt = new Date().toISOString().slice(0, 10) detail.lastMessagePreview = msg.body.slice(0, 80) // sync summary ticket const ticket = state.value.tickets.find(t => t.id === ticketId) if (ticket) { ticket.messageCount = detail.messageCount ticket.updatedAt = detail.updatedAt ticket.lastMessagePreview = detail.lastMessagePreview } } // ── Mock Routing ── const routingKeywords: Record = { collections: ['pago', 'factura', 'cobro', 'recibo', 'transferencia', 'mora'], claims: ['siniestro', 'accidente', 'robo', 'daño', 'choque', 'grúa', 'colisión'], sales: ['cotización', 'seguro nuevo', 'precio', 'cuánto sale', 'cobertura'], renewals: ['renovación', 'vencimiento', 'prórroga', 'vigencia'], operations: ['endoso', 'certificado', 'modificar', 'cambio de beneficiario'], open_pool: [], } function simulateRouting(channel: SupportChannel, body: string): { tier: RoutingTier; queue: RoutedQueue; confidence: number } { const lower = body.toLowerCase() // Tier 2: keyword matching for (const [queue, keywords] of Object.entries(routingKeywords) as [RoutedQueue, string[]][]) { if (!keywords.length) continue const matched = keywords.filter(kw => lower.includes(kw)) if (matched.length > 0) { const confidence = Math.min(0.95, 0.6 + matched.length * 0.1) return { tier: 'tier2_rule', queue, confidence } } } // Tier 3: no match → open pool return { tier: 'tier3_open', queue: 'open_pool', confidence: 0.3 } } // ── Routing Rules CRUD ── function toggleRule(ruleId: string) { const rule = state.value.routingRules.find(r => r.id === ruleId) if (rule) rule.enabled = !rule.enabled } function updateRule(ruleId: string, updates: Partial) { const rule = state.value.routingRules.find(r => r.id === ruleId) if (rule) Object.assign(rule, updates) } function getDetail(ticketId: string): SupportTicketDetail | undefined { return state.value.details[ticketId] } return { state, // computed openTickets, unresolvedCount, breachedCount, unassignedCount, openPoolCount, inProgressCount, kpis, // CRUD updateStatus, assignTicket, addMessage, getDetail, // routing simulateRouting, toggleRule, updateRule, } }