/** * Tickets SLA + Priority Score v0 — port do ResultsDesk (React) * Spec negócio Roger: SLA_SETTINGS + fórmula composta origem × idle */ const TicketsSla = { SETTINGS: { ONBOARD: { response: 2, resolution: 72 }, DESK: { response: 1, resolution: 8 }, SECURITY: { response: 0.5, resolution: 4 }, AGENTIC_OPS: { response: 0.5, resolution: 4 }, }, PRIORITY_CRITICAL: 7, PRIORITY_HIGH: 4, PRIORITY_WARN: 2, mapTenant(t) { const src = t.source || ''; const ev = t.event || ''; if (src === 'wazuh' || ev === 'wazuh.alert' || src === 'vm112-security' || (t.subject || '').startsWith('[security]')) { return 'SECURITY'; } if (src === 'vm112-onboard' || ev.startsWith('onboarding') || ev.startsWith('domain.') || ev.startsWith('dns.')) { return 'ONBOARD'; } if (src === 'agentic-ops' || (t.subject || '').toLowerCase().includes('[agentic')) { return 'AGENTIC_OPS'; } return 'DESK'; }, hoursSince(iso) { if (!iso) return 0; try { return Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 3600000)); } catch { return 0; } }, isInternalDomain(domain) { const d = String(domain || '').toLowerCase(); return d.includes('itecnologys.com') || d.includes('ligbox.com.br'); }, computePriority(t) { const tenant = this.mapTenant(t); const sla = this.SETTINGS[tenant] || { response: 2, resolution: 24 }; const totalAgeHours = this.hoursSince(t.created_at); const lastEvent = t._lastEventAt || null; const idleHours = t._idleHours != null ? t._idleHours : this.hoursSince(lastEvent || t.created_at); const wazuhLevel = t.severity != null ? Number(t.severity) : null; const isLead = t.crm_track === 'lead' || t.is_lead; let originScore = 1.0; if (tenant === 'SECURITY' && wazuhLevel != null && wazuhLevel >= 1) originScore = 4.0; else if (this.isInternalDomain(t.domain || t.email)) originScore = 3.0; else if (isLead) originScore = 2.0; let idleScore = 0.5; if (idleHours >= sla.resolution * 2) idleScore = 2.5; else if (idleHours >= sla.resolution) idleScore = 2.0; else if (idleHours >= sla.response * 2) idleScore = 1.5; else if (idleHours >= sla.response) idleScore = 1.0; const priorityScore = parseFloat((originScore * idleScore).toFixed(1)); let responseSla = 'OK'; let resolutionSla = 'OK'; if (idleHours > sla.response) responseSla = 'WARNING'; if (idleHours > sla.resolution) resolutionSla = `STALE (${sla.resolution}h+)`; let priorityBand = 'info'; if (priorityScore >= this.PRIORITY_CRITICAL) priorityBand = 'critical'; else if (priorityScore >= this.PRIORITY_HIGH) priorityBand = 'high'; else if (priorityScore >= this.PRIORITY_WARN) priorityBand = 'warn'; return { ...t, _tenant: tenant, _priorityScore: priorityScore, _priorityBand: priorityBand, _totalAgeHours: totalAgeHours, _idleHours: idleHours, _responseSla: responseSla, _resolutionSla: resolutionSla, _slaLimits: sla, }; }, processList(tickets) { return tickets .map((t) => this.computePriority(t)) .sort((a, b) => b._priorityScore - a._priorityScore); }, computeOverview(tickets) { const rows = tickets.map((t) => this.computePriority(t)); const active = rows.filter((t) => !['closed'].includes(t.status)); const resolved = rows.filter((t) => ['resolved', 'closed'].includes(t.status)); const slaOk = active.filter((t) => t._responseSla === 'OK' && t._resolutionSla === 'OK').length; const slaPct = active.length ? Math.round((slaOk / active.length) * 100) : 100; const mttrSamples = resolved.map((t) => t._totalAgeHours).filter((h) => h > 0); const mttr = mttrSamples.length ? (mttrSamples.reduce((a, b) => a + b, 0) / mttrSamples.length).toFixed(1) : '—'; const responseSamples = rows .filter((t) => t.assigned_at || t.assisted_at) .map((t) => { try { const start = new Date(t.created_at).getTime(); const end = new Date(t.assigned_at || t.assisted_at).getTime(); return Math.max(0, Math.round((end - start) / 3600000)); } catch { return null; } }) .filter((h) => h != null); const avgResponse = responseSamples.length ? (responseSamples.reduce((a, b) => a + b, 0) / responseSamples.length).toFixed(1) : (active.length ? (active.reduce((s, t) => s + t._idleHours, 0) / active.length).toFixed(1) : '—'); const buckets = { critical: 0, high: 0, warn: 0, info: 0 }; for (const t of rows) buckets[t._priorityBand] = (buckets[t._priorityBand] || 0) + 1; const criticalWaiting = active.filter( (t) => t._priorityBand === 'critical' && ['open', 'escalated', 'assisting'].includes(t.status), ).length; return { slaPct, mttr, avgResponse, buckets, criticalWaiting, total: rows.length, }; }, getInitials(name) { if (!name) return '—'; const parts = String(name).trim().split(/\s+/); return ((parts[0]?.[0] || '') + (parts[1]?.[0] || parts[0]?.[1] || '')).toUpperCase() || '—'; }, assigneeLabel(t, userMap = {}) { const raw = t.assisted_by || t.assigned_to || ''; if (!raw) return { label: 'Sem atribuição', initials: '—' }; const label = userMap[raw] || raw.replace(/[._]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); return { label, initials: this.getInitials(label) }; }, }; window.TicketsSla = TicketsSla;