import { ref } from 'vue' import { HUB_URL } from 'src/config/hub' // ───────────────────────────────────────────────────────────────────────────── // SLA côté Ops : charge les politiques (par FILE=issue_type + PRIORITÉ) depuis le hub, // et calcule l'état d'un ticket (réponse d'abord tant que non répondu, puis résolution). // const { slaFor, savePolicies, policies, defaults } = useSla() // slaFor(ticket) → { state:'ok'|'at_risk'|'breached'|'met'|'none', kind, label, remainMins } // ───────────────────────────────────────────────────────────────────────────── let _policies = null let _defaults = {} let _loadP = null const policies = ref(null) const defaults = ref({}) function loadPolicies () { if (_policies) return Promise.resolve(_policies) if (!_loadP) { _loadP = fetch(`${HUB_URL}/sla/policies`).then(r => r.ok ? r.json() : {}) .then(d => { _policies = d.policies || {}; _defaults = d.defaults || {}; policies.value = _policies; defaults.value = _defaults; return _policies }) .catch(() => { _policies = {}; policies.value = {}; return {} }) } return _loadP } async function savePolicies (pol) { _policies = pol; policies.value = pol const r = await fetch(`${HUB_URL}/sla/policies`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ policies: pol }) }) return r.ok } function policyFor (issueType, priority) { const p = policies.value || _policies || {} // policies.value = ref réactif → recalcule quand les politiques se chargent return (p[issueType] && p[issueType][priority]) || (p._default && p._default[priority]) || null } function fmt (mins) { const a = Math.abs(Math.round(mins)) if (a < 60) return a + ' min' if (a < 1440) { const h = Math.floor(a / 60); const m = a % 60; return h + ' h' + (m ? ' ' + m : '') } const j = Math.floor(a / 1440); const h = Math.floor((a % 1440) / 60); return j + ' j' + (h ? ' ' + h + ' h' : '') } export function useSla () { loadPolicies() function slaFor (t) { if (!t) return { state: 'none' } const pol = policyFor(t.issue_type, t.priority) if (!pol) return { state: 'none' } const od = t.opening_date || t.creation if (!od) return { state: 'none' } const opening = new Date(String(od).length <= 10 ? od + 'T00:00:00' : od) if (isNaN(opening.getTime())) return { state: 'none' } if (['Resolved', 'Closed'].includes(t.status)) return { state: 'met', label: 'SLA clos' } const now = Date.now() const band = (remain, target, kind, verb) => { if (remain < 0) return { state: 'breached', kind, label: verb + ' en retard ' + fmt(remain), remainMins: remain } if (remain < target * 0.25) return { state: 'at_risk', kind, label: verb + ' dans ' + fmt(remain), remainMins: remain } return { state: 'ok', kind, label: verb + ' dans ' + fmt(remain), remainMins: remain } } // Réponse (1ʳᵉ réponse) prioritaire tant que pas encore répondu if (!t.first_responded_on && pol.response) return band((opening.getTime() + pol.response * 60000 - now) / 60000, pol.response, 'response', 'Réponse') // Sinon résolution if (pol.resolution) return band((opening.getTime() + pol.resolution * 60000 - now) / 60000, pol.resolution, 'resolution', 'Résolution') return { state: 'none' } } return { slaFor, loadPolicies, savePolicies, policies, defaults } } // Couleurs de pastille par état (réutilisable). export const SLA_BADGE = { breached: { color: 'red-2', text: 'red-9', icon: 'error' }, at_risk: { color: 'orange-2', text: 'orange-9', icon: 'schedule' }, ok: { color: 'green-2', text: 'green-9', icon: 'schedule' }, met: { color: 'grey-3', text: 'grey-7', icon: 'check' }, }