WIP jordan
This commit is contained in:
184
app/composables/useSupportTickets.ts
Normal file
184
app/composables/useSupportTickets.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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<string, SupportTicketDetail>
|
||||
routingRules: RoutingRule[]
|
||||
}
|
||||
|
||||
function buildDefaults(): SupportState {
|
||||
return {
|
||||
tickets: [...MOCK_SUPPORT_TICKETS],
|
||||
details: { ...MOCK_TICKET_DETAILS },
|
||||
routingRules: [...MOCK_ROUTING_RULES],
|
||||
}
|
||||
}
|
||||
|
||||
export function useSupportTickets() {
|
||||
const state = useLocalStorageRef<SupportState>('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<TicketMessage, 'id' | 'timestamp'>) {
|
||||
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<RoutedQueue, string[]> = {
|
||||
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<RoutingRule>) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user