/** * AgileHub โ€” Bug Reporter Widget * Embeddabile in qualsiasi prodotto (AllTax, TRPG, DFM, WMS, ecc.) * * Features: * - Bottone ๐Ÿ› segnalazione bug (testo + voce) * - Ctrl+V paste screenshot dalla clipboard * - Bottone ๐Ÿ“ธ screenshot rapido della videata * - Drag & drop immagini * - Tab "Mie Segnalazioni" / "Risolte" * - Campanella ๐Ÿ”” notifiche con badge * * Uso: * */ (function() { 'use strict'; // ==================== CONFIG ==================== const script = document.currentScript || document.querySelector('script[data-product]'); const WIDGET_VERSION = '1.4.15'; const WIDGET_BUILD = '20260415d'; const CFG = { apiUrl: (script && script.dataset.apiUrl) || 'http://localhost', apiKey: (script && script.dataset.apiKey) || '', product: (script && script.dataset.product) || '', tenantId: (script && script.dataset.tenantId) || '1', userName: (script && script.dataset.userName) || '', userEmail: (script && script.dataset.userEmail) || '', userRole: (script && script.dataset.userRole) || '', lang: (script && script.dataset.lang) || 'it' }; // L'API รจ sempre via proxy /api (sia per la dashboard che per i prodotti esterni). const API = CFG.apiUrl + '/api'; // Headers standard per tutte le chiamate API function _headers(json) { const h = { 'x-tenant-id': CFG.tenantId }; if (CFG.apiKey) h['X-API-Key'] = CFG.apiKey; if (json) h['Content-Type'] = 'application/json'; return h; } // Auto-detect utente dal contesto dell'app (se data-user-email non รจ nel tag) function _detectUser() { if (CFG.userEmail) return; try { // 1. sessionStorage 'user' (pattern TRPG โ€” JSON con email/name/role) const userJson = sessionStorage.getItem('user'); if (userJson) { const u = JSON.parse(userJson); if (u && u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; } } // 2. api.getUser() (pattern TRPG/AllTax โ€” globale) if (typeof api !== 'undefined') { const u = (typeof api.getUser === 'function' ? api.getUser() : api.user) || {}; if (u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; } } // 3. JWT decode da access_token / nexus_token const token = sessionStorage.getItem('access_token') || sessionStorage.getItem('nexus_token') || localStorage.getItem('nexus_token'); if (token && token.includes('.')) { const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); if (payload.email) { CFG.userEmail = payload.email; CFG.userName = payload.name || payload.full_name || CFG.userName; CFG.userRole = payload.role || CFG.userRole; } } } catch(e) { /* silent */ } } // Ritenta: lo script รจ nel HEAD, l'utente potrebbe non essere ancora loggato setTimeout(_detectUser, 500); setTimeout(_detectUser, 3000); setTimeout(_detectUser, 8000); // ==================== STATE ==================== let _attachments = []; let _notifCount = 0; let _notifPollTimer = null; let _supportSessionId = null; let _chatMessages = []; let _chatMode = false; // AI chat e nel widget separato ai-assistant.js let _chatLoading = false; // ==================== STYLES ==================== const STYLES = ` #nx-bar{position:fixed;bottom:94px;right:24px;display:flex;flex-direction:column;gap:14px;z-index:99990;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif} #nx-bar button{width:56px;height:56px;border-radius:50%;border:none;cursor:pointer;font-size:22px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,.25);transition:transform .15s} #nx-bar button:hover{transform:scale(1.1)} #nx-bug-btn{background:#ef4444;color:#fff} #nx-bell-btn{background:#3b82f6;color:#fff;position:relative} #nx-bell-badge{position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;border-radius:10px;min-width:18px;height:18px;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 4px} #nx-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99991;display:flex;align-items:center;justify-content:center} #nx-modal{background:#fff;border-radius:14px;width:92%;max-width:500px;max-height:92vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.3)} #nx-modal *{box-sizing:border-box} .nx-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb} .nx-header h3{margin:0;font-size:17px;font-weight:600} .nx-close{cursor:pointer;font-size:20px;color:#999;background:none;border:none;width:auto;height:auto;box-shadow:none} .nx-close:hover{color:#333;transform:none} .nx-tabs{display:flex;border-bottom:1px solid #e5e7eb} .nx-tab{flex:1;padding:10px;text-align:center;font-size:13px;font-weight:500;cursor:pointer;border:none;background:none;color:#6b7280;border-bottom:2px solid transparent;transition:all .15s} .nx-tab.active{color:#3b82f6;border-bottom-color:#3b82f6} .nx-body{flex:1;overflow-y:auto;padding:16px 20px} .nx-row{display:flex;gap:8px;margin-bottom:10px} .nx-select,.nx-input,.nx-textarea{width:100%;padding:9px 12px;border-radius:8px;border:1px solid #d1d5db;font-size:14px;font-family:inherit} .nx-textarea{height:90px;resize:vertical} .nx-textarea.drag-over{border-color:#3b82f6;background:#eff6ff} .nx-btn{padding:10px;border-radius:8px;border:1px solid #d1d5db;cursor:pointer;background:#f9fafb;font-size:14px;text-align:center;flex:1} .nx-btn:hover{background:#f3f4f6} .nx-btn-primary{background:#3b82f6;color:#fff;border:none;font-weight:600;width:100%;padding:12px;font-size:15px} .nx-btn-primary:hover{background:#2563eb} .nx-btn-primary:disabled{opacity:.5;cursor:not-allowed} .nx-att-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px} .nx-att-item{position:relative;width:60px;height:60px;border-radius:6px;overflow:hidden;border:1px solid #e5e7eb} .nx-att-item img{width:100%;height:100%;object-fit:cover} .nx-att-remove{position:absolute;top:-4px;right:-4px;width:18px;height:18px;border-radius:50%;background:#ef4444;color:#fff;font-size:11px;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1} .nx-ctx{background:#f8fafc;border-radius:8px;padding:8px 12px;margin-bottom:10px;font-size:12px;color:#6b7280} .nx-result{margin-top:12px;padding:12px;border-radius:8px;font-size:14px} .nx-result.ok{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534} .nx-result.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b} .nx-ticket{padding:12px;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;cursor:pointer;transition:background .1s} .nx-ticket:hover{background:#f9fafb} .nx-ticket-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px} .nx-ticket-id{font-size:12px;font-weight:600;color:#3b82f6} .nx-ticket-status{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500} .nx-ticket-subject{font-size:14px;font-weight:500;margin-bottom:4px} .nx-ticket-meta{font-size:12px;color:#9ca3af} .nx-ticket-ai{font-size:13px;color:#7c3aed;background:#f5f3ff;padding:8px;border-radius:6px;margin-top:6px} .nx-notif{padding:10px 12px;border-bottom:1px solid #f3f4f6;cursor:pointer;transition:background .1s} .nx-notif:hover{background:#f9fafb} .nx-notif.unread{background:#eff6ff} .nx-notif-title{font-size:14px;font-weight:500;margin-bottom:2px} .nx-notif-body{font-size:13px;color:#6b7280} .nx-notif-meta{font-size:11px;color:#9ca3af;margin-top:4px} .nx-notif-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px} .nx-notif-dot.BUG_FIX{background:#22c55e}.nx-notif-dot.UPDATE{background:#3b82f6} .nx-notif-dot.ALERT{background:#ef4444}.nx-notif-dot.INFO{background:#9ca3af} .nx-empty{text-align:center;padding:30px;color:#9ca3af;font-size:14px} .nx-chat-area{flex:1;overflow-y:auto;padding:12px;min-height:200px;max-height:50vh;display:flex;flex-direction:column;gap:8px} .nx-chat-msg{max-width:85%;padding:10px 14px;border-radius:14px;font-size:14px;line-height:1.5;word-wrap:break-word;animation:nxFadeIn .2s} .nx-chat-msg.user{align-self:flex-end;background:#4f46e5;color:#fff;border-bottom-right-radius:4px} .nx-chat-msg.ai{align-self:flex-start;background:#f3f4f6;color:#1f2937;border-bottom-left-radius:4px} .nx-chat-msg.system{align-self:center;background:#fef3c7;color:#92400e;font-size:13px;border-radius:8px;text-align:center} .nx-chat-input{display:flex;gap:8px;padding:12px;border-top:1px solid #e5e7eb} .nx-chat-input input{flex:1;padding:10px 14px;border:1px solid #d1d5db;border-radius:20px;font-size:14px;font-family:inherit;outline:none} .nx-chat-input input:focus{border-color:#4f46e5} .nx-chat-input button{width:40px;height:40px;border-radius:50%;border:none;background:#4f46e5;color:#fff;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center} .nx-chat-input button:disabled{opacity:.4;cursor:not-allowed} .nx-chat-actions{display:flex;gap:8px;padding:0 12px 12px;justify-content:center} .nx-chat-actions button{padding:8px 16px;border-radius:8px;border:1px solid #d1d5db;background:#f9fafb;font-size:13px;cursor:pointer;font-family:inherit} .nx-chat-actions button:hover{background:#f3f4f6} .nx-chat-actions .resolve{border-color:#22c55e;color:#16a34a} .nx-chat-actions .classic{border-color:#3b82f6;color:#2563eb} .nx-typing{display:flex;gap:4px;padding:6px 14px;align-self:flex-start} .nx-typing span{width:6px;height:6px;border-radius:50%;background:#9ca3af;animation:nxBounce .6s infinite alternate} .nx-typing span:nth-child(2){animation-delay:.15s} .nx-typing span:nth-child(3){animation-delay:.3s} @keyframes nxFadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}} @keyframes nxBounce{to{transform:translateY(-4px);opacity:.4}} .nx-status-OPEN{background:#dbeafe;color:#1d4ed8} .nx-status-IN_PROGRESS{background:#fef3c7;color:#92400e} .nx-status-PENDING{background:#f3f4f6;color:#4b5563} .nx-status-RESOLVED{background:#d1fae5;color:#065f46} .nx-status-CLOSED{background:#e5e7eb;color:#6b7280} `; // ==================== INIT ==================== function init() { // Inject styles const style = document.createElement('style'); style.textContent = STYLES; document.head.appendChild(style); // Bottom bar: ๐Ÿ›Ÿ supporto + ๐Ÿ”” notifiche // Standard suite Agile: bug=fa-life-ring "Chiedi supporto" / bell=fa-bell const bar = document.createElement('div'); bar.id = 'nx-bar'; bar.innerHTML = ` `; document.body.appendChild(bar); document.getElementById('nx-bug-btn').onclick = () => openModal('new'); document.getElementById('nx-bell-btn').onclick = () => openModal('notif'); // Hook campanella topbar del prodotto (se esiste) const topBell = document.getElementById('btn-notifications'); if (topBell) { // Rimuovi il vecchio dropdown notifiche del prodotto (evita doppia finestra) const oldDropdown = document.getElementById('notif-dropdown'); if (oldDropdown) oldDropdown.remove(); // Clona per rimuovere vecchi listener const newBell = topBell.cloneNode(true); topBell.parentNode.replaceChild(newBell, topBell); newBell.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); openModal('notif'); }); window._nxTopBadge = function(c) { const b = newBell.querySelector('.badge') || document.getElementById('worklist-badge'); if (b) { b.textContent = c > 99 ? '99+' : c; b.style.display = c > 0 ? '' : 'none'; } }; } // Start notification polling pollNotifications(); _notifPollTimer = setInterval(pollNotifications, 60000); } // ==================== NOTIFICATION POLLING ==================== async function pollNotifications() { try { const res = await fetch(`${API}/notifications/unread-count?product=${CFG.product}`, { headers: _headers() }); const d = await res.json(); if (d.success) { _notifCount = d.data.unread; const badge = document.getElementById('nx-bell-badge'); if (badge) { badge.style.display = _notifCount > 0 ? 'flex' : 'none'; badge.textContent = _notifCount > 9 ? '9+' : String(_notifCount); } // Aggiorna anche campanella topbar del prodotto (se hookata) if (window._nxTopBadge) window._nxTopBadge(_notifCount); } } catch(e) { /* silent */ } } // ==================== MODAL ==================== function openModal(tab) { closeModal(); _attachments = []; _chatMode = false; // AI chat e nel widget separato ai-assistant.js _supportSessionId = null; _chatMessages = []; // Nasconde FAB ARIA quando il modale segnalazioni e aperto var aiaFab = document.getElementById('ai-chat-fab'); if (aiaFab) aiaFab.style.display = 'none'; const bg = document.createElement('div'); bg.id = 'nx-modal-bg'; bg.onclick = (e) => { if (e.target === bg) closeModal(); }; bg.innerHTML = `

Segnalazioni

`; document.body.appendChild(bg); // Tab click bg.querySelectorAll('.nx-tab').forEach(t => { t.onclick = () => switchTab(t.dataset.tab); }); switchTab(tab || 'new'); } function closeModal() { document.getElementById('nx-modal-bg')?.remove(); // Rimostra FAB ARIA quando il modale segnalazioni si chiude var aiaFab = document.getElementById('ai-chat-fab'); if (aiaFab) aiaFab.style.display = 'flex'; } function switchTab(tab) { document.querySelectorAll('.nx-tab').forEach(t => { t.classList.toggle('active', t.dataset.tab === tab); }); const body = document.getElementById('nx-body'); if (!body) return; if (tab === 'new') { if (_chatMode && !_supportSessionId) renderChatMode(body); else if (_chatMode && _supportSessionId) renderChatUI(body); else renderNewForm(body); } else if (tab === 'mine') renderMyTickets(body, 'ACTIVE'); else if (tab === 'done') renderMyTickets(body, 'DONE'); else if (tab === 'notif') renderNotifications(body); } // ==================== TAB: CHAT MODE (Pre-Ticket AI Dialog) ==================== async function renderChatMode(body) { _chatMessages = []; _supportSessionId = null; body.innerHTML = '
Avvio assistente...
'; try { const res = await fetch(`${API}/support-sessions`, { method: 'POST', headers: _headers(true), body: JSON.stringify({ product: CFG.product, callerEmail: CFG.userEmail, callerName: CFG.userName }) }); const d = await res.json(); if (d.success) { _supportSessionId = d.data.sessionId; _chatMessages.push({ role: 'ai', text: d.data.welcomeMessage }); } else { _chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' }); } } catch(e) { _chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' }); } renderChatUI(body); } function renderChatUI(body) { body.style.padding = '0'; body.innerHTML = `
`; // Render messages const area = document.getElementById('nx-chat-area'); _chatMessages.forEach(m => appendChatBubble(area, m.role, m.text)); area.scrollTop = area.scrollHeight; // Enter to send document.getElementById('nx-chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !_chatLoading) window._nxChatSend(); }); } function appendChatBubble(area, role, text) { const div = document.createElement('div'); div.className = `nx-chat-msg ${role}`; div.textContent = text; area.appendChild(div); } function showTyping(area) { const div = document.createElement('div'); div.className = 'nx-typing'; div.id = 'nx-typing'; div.innerHTML = ''; area.appendChild(div); area.scrollTop = area.scrollHeight; } function hideTyping() { document.getElementById('nx-typing')?.remove(); } window._nxChatSend = async () => { const input = document.getElementById('nx-chat-input'); const text = input.value.trim(); if (!text || _chatLoading) return; input.value = ''; _chatLoading = true; const area = document.getElementById('nx-chat-area'); _chatMessages.push({ role: 'user', text }); appendChatBubble(area, 'user', text); area.scrollTop = area.scrollHeight; const sendBtn = document.getElementById('nx-chat-send'); sendBtn.disabled = true; showTyping(area); try { const res = await fetch(`${API}/support-sessions/${_supportSessionId}/message`, { method: 'POST', headers: _headers(true), body: JSON.stringify({ content: text }) }); const d = await res.json(); hideTyping(); if (d.success) { const reply = d.data.reply; _chatMessages.push({ role: 'ai', text: reply }); appendChatBubble(area, 'ai', reply); // Check for escalation/forward action from AI tool calls if (d.data.action) { if (d.data.action.type === 'ESCALATED') { const ticketId = d.data.action.data?.ticketId; _chatMessages.push({ role: 'system', text: `Ticket #${ticketId} creato. Il team lo prendera in carico.` }); appendChatBubble(area, 'system', `Ticket #${ticketId} creato. Il team lo prendera in carico.`); } if (d.data.action.type === 'FORWARDED') { const expertName = d.data.action.data?.expertName || 'esperto'; _chatMessages.push({ role: 'system', text: `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.` }); appendChatBubble(area, 'system', `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.`); } } } else { _chatMessages.push({ role: 'ai', text: 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?' }); appendChatBubble(area, 'ai', 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?'); } } catch(e) { hideTyping(); _chatMessages.push({ role: 'ai', text: 'Connessione non disponibile. Prova la segnalazione classica.' }); appendChatBubble(area, 'ai', 'Connessione non disponibile. Prova la segnalazione classica.'); } area.scrollTop = area.scrollHeight; _chatLoading = false; sendBtn.disabled = false; input.focus(); }; window._nxChatResolve = async () => { if (_supportSessionId) { try { await fetch(`${API}/support-sessions/${_supportSessionId}/resolve`, { method: 'POST', headers: _headers(true), body: JSON.stringify({ rating: 5 }) }); } catch(e) { /* silent */ } } const area = document.getElementById('nx-chat-area'); if (area) { appendChatBubble(area, 'system', 'Grazie! Se hai altre domande, siamo qui. ๐Ÿ˜Š'); area.scrollTop = area.scrollHeight; } // Reset after 2 seconds setTimeout(() => { _supportSessionId = null; _chatMessages = []; _chatMode = true; closeModal(); }, 2000); }; window._nxChatSkip = () => { _chatMode = false; const body = document.getElementById('nx-body'); if (body) { body.style.padding = '16px 20px'; renderNewForm(body); } }; // ==================== TAB: NUOVA SEGNALAZIONE ==================== function renderNewForm(body) { const ctx = getContext(); body.innerHTML = `
๐Ÿ“ ${ctx.pageUrl.split('#')[1] || ctx.pageUrl.split('/').pop()} ${ctx.companyName ? '| ๐Ÿข ' + ctx.companyName : ''}
`; // Paste handler const desc = document.getElementById('nx-desc'); desc.addEventListener('paste', handlePaste); // Drag & drop desc.addEventListener('dragover', (e) => { e.preventDefault(); desc.classList.add('drag-over'); }); desc.addEventListener('dragleave', () => desc.classList.remove('drag-over')); desc.addEventListener('drop', (e) => { e.preventDefault(); desc.classList.remove('drag-over'); if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }); } // ==================== PASTE / SCREENSHOT / ATTACH ==================== function handlePaste(e) { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault(); const blob = item.getAsFile(); blobToAttachment(blob, `screenshot_${Date.now()}.png`); } } } function blobToAttachment(blob, filename) { const reader = new FileReader(); reader.onload = () => { _attachments.push({ filename: filename, mimeType: blob.type, size: blob.size, data: reader.result.split(',')[1], // base64 without prefix preview: reader.result }); renderAttachments(); }; reader.readAsDataURL(blob); } function addFiles(files) { for (const file of files) { if (_attachments.length >= 10) break; blobToAttachment(file, file.name); } } window._nxAttachFiles = (files) => addFiles(files); window._nxScreenshot = async () => { const btn = document.getElementById('nx-screenshot-btn'); try { // Hide modal temporarily const modal = document.getElementById('nx-modal-bg'); if (modal) modal.style.display = 'none'; btn.textContent = 'โณ Cattura...'; // Wait a frame for modal to hide await new Promise(r => setTimeout(r, 100)); // Use html2canvas if available, otherwise use Screen Capture API if (typeof html2canvas !== 'undefined') { const canvas = await html2canvas(document.body, { useCORS: true, scale: 1 }); canvas.toBlob((blob) => { if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`); if (modal) modal.style.display = 'flex'; btn.textContent = '๐Ÿ“ธ Screenshot'; }, 'image/png'); } else if (navigator.mediaDevices?.getDisplayMedia) { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: 'screen' } }); const track = stream.getVideoTracks()[0]; const imageCapture = new ImageCapture(track); const bitmap = await imageCapture.grabFrame(); track.stop(); const canvas = document.createElement('canvas'); canvas.width = bitmap.width; canvas.height = bitmap.height; canvas.getContext('2d').drawImage(bitmap, 0, 0); canvas.toBlob((blob) => { if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`); if (modal) modal.style.display = 'flex'; btn.textContent = '๐Ÿ“ธ Screenshot'; }, 'image/png'); } else { // Fallback: canvas from body (basic) if (modal) modal.style.display = 'flex'; btn.textContent = '๐Ÿ“ธ Screenshot'; alert('Screenshot non supportato in questo browser. Usa Ctrl+V per incollare.'); } } catch(err) { const modal = document.getElementById('nx-modal-bg'); if (modal) modal.style.display = 'flex'; btn.textContent = '๐Ÿ“ธ Screenshot'; if (err.name !== 'NotAllowedError') { console.warn('Screenshot error:', err); } } }; function renderAttachments() { const list = document.getElementById('nx-att-list'); if (!list) return; list.innerHTML = _attachments.map((a, i) => `
${a.filename}
`).join(''); } window._nxRemoveAtt = (i) => { _attachments.splice(i, 1); renderAttachments(); }; // ==================== VOICE ==================== window._nxVoice = () => { if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { alert('Riconoscimento vocale non supportato. Usa Chrome.'); return; } const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); recognition.lang = CFG.lang === 'it' ? 'it-IT' : CFG.lang + '-' + CFG.lang.toUpperCase(); recognition.continuous = false; const btn = document.getElementById('nx-voice-btn'); btn.textContent = '๐Ÿ”ด Parla ora...'; btn.style.background = '#fee2e2'; recognition.onresult = (e) => { const desc = document.getElementById('nx-desc'); desc.value += (desc.value ? '\n' : '') + e.results[0][0].transcript; btn.textContent = '๐ŸŽค Parla'; btn.style.background = ''; }; recognition.onerror = recognition.onend = () => { btn.textContent = '๐ŸŽค Parla'; btn.style.background = ''; }; recognition.start(); }; // ==================== SUBMIT ==================== window._nxSubmit = async () => { const desc = document.getElementById('nx-desc')?.value?.trim(); const tipo = document.getElementById('nx-tipo')?.value; const prio = document.getElementById('nx-prio')?.value; const submitBtn = document.getElementById('nx-submit'); const result = document.getElementById('nx-result'); if (!desc && _attachments.length === 0) { alert('Descrivi il problema o allega uno screenshot'); return; } submitBtn.disabled = true; submitBtn.textContent = 'Invio in corso...'; const ctx = getContext(); const versionTag = `[v${WIDGET_VERSION} build ${WIDGET_BUILD}]`; const payload = { transcription: desc ? `${desc}\n\n${versionTag}` : `Segnalazione con screenshot ${versionTag}`, callerName: CFG.userName || ctx.userName, callerEmail: CFG.userEmail || ctx.userEmail, kind: tipo, priority: prio, queue: 'SUPPORT', product: CFG.product, pageUrl: ctx.pageUrl, reporterRole: CFG.userRole || ctx.userRole, lang: CFG.lang, appVersion: WIDGET_VERSION, appBuild: WIDGET_BUILD }; try { // 1. Create ticket const res = await fetch(`${API}/tickets/voice-report`, { method: 'POST', headers: _headers(true), body: JSON.stringify(payload) }); const data = await res.json(); if (data.success) { const ticketId = data.data.ticket.id; // 2. Attach files if (_attachments.length > 0) { await fetch(`${API}/tickets/${ticketId}/attachments-batch`, { method: 'POST', headers: _headers(true), body: JSON.stringify({ attachments: _attachments.map(a => ({ filename: a.filename, mimeType: a.mimeType, size: a.size, data: a.data })) }) }); } result.className = 'nx-result ok'; result.style.display = 'block'; result.innerHTML = `โœ… Segnalazione #${ticketId} inviata!
${data.data.ticket.aiResponse || 'Grazie, il team la prendera in carico.'}`; submitBtn.textContent = 'โœ“ Inviato'; // Play audio if available if (data.data.voice?.audioBase64) { try { new Audio(data.data.voice.audioBase64).play(); } catch(e) {} } } else { throw new Error(data.error?.message || 'Errore'); } } catch(err) { result.className = 'nx-result err'; result.style.display = 'block'; result.textContent = 'โŒ ' + (err.message || 'Servizio non raggiungibile'); submitBtn.disabled = false; submitBtn.textContent = 'Invia Segnalazione'; } }; // ==================== TAB: MIE SEGNALAZIONI / RISOLTE ==================== async function renderMyTickets(body, statusFilter) { body.innerHTML = '
Caricamento...
'; const email = CFG.userEmail; if (!email) { body.innerHTML = '
Email utente non configurata
'; return; } try { const res = await fetch(`${API}/tickets/my?callerEmail=${encodeURIComponent(email)}&status=${statusFilter}&product=${CFG.product}`, { headers: _headers() }); const data = await res.json(); if (!data.success || !data.data.tickets.length) { body.innerHTML = `
${statusFilter === 'DONE' ? 'Nessuna segnalazione risolta' : 'Nessuna segnalazione aperta'}
`; return; } body.innerHTML = data.data.tickets.map(t => `
#${t.id} ${t.status.replace('_', ' ')}
${t.subject || t.operativeSummary?.substring(0, 80) || 'Senza oggetto'}
${t.priority} | ${timeAgo(t.createdAt)}
${t.aiResponse ? `` : ''} ${t.messages?.length ? `` : ''} ${t.status === 'RESOLVED' ? `` : ''}
`).join(''); } catch(err) { body.innerHTML = '
Servizio non raggiungibile
'; } } window._nxToggleTicket = (el) => { el.querySelectorAll('.nx-ticket-ai').forEach(d => { d.style.display = d.style.display === 'none' ? 'block' : 'none'; }); }; window._nxReopenTicket = async (ticketId, btn) => { btn.disabled = true; btn.textContent = 'Riapertura...'; try { const res = await fetch(`${API}/tickets/${ticketId}/status`, { method: 'PATCH', headers: _headers(true), body: JSON.stringify({ status: 'OPEN', performedBy: CFG.userEmail || 'utente' }) }); const data = await res.json(); if (data.success) { btn.textContent = 'Riaperto!'; btn.style.background = '#d1fae5'; btn.style.color = '#065f46'; btn.style.borderColor = '#10b981'; // Aggiunge un messaggio al ticket con la segnalazione dell'utente await fetch(`${API}/tickets/${ticketId}/message`, { method: 'POST', headers: _headers(true), body: JSON.stringify({ content: 'Riaperto dall\'utente: il problema non era risolto.', role: 'USER' }) }); setTimeout(() => renderTickets(btn.closest('.nx-body') || document.querySelector('.nx-body')), 1000); } else { btn.textContent = data.error?.message || 'Errore'; btn.style.background = '#fee2e2'; } } catch(e) { btn.textContent = 'Errore connessione'; } }; // ==================== TAB: NOTIFICHE ==================== async function renderNotifications(body) { body.innerHTML = '
Caricamento...
'; try { const res = await fetch(`${API}/notifications?product=${CFG.product}&limit=30`, { headers: _headers() }); const data = await res.json(); if (!data.success || !data.data.notifications.length) { body.innerHTML = '
Nessuna notifica
'; return; } let html = `
`; html += data.data.notifications.map(n => `
${n.title}
${n.body ? `
${n.body}
` : ''}
${n.type} | ${timeAgo(n.createdAt)}
`).join(''); body.innerHTML = html; } catch(err) { body.innerHTML = '
Servizio non raggiungibile
'; } } window._nxReadNotif = async (id, el) => { try { await fetch(`${API}/notifications/${id}/read`, { method: 'PUT', headers: _headers() }); el.classList.remove('unread'); pollNotifications(); } catch(e) {} }; window._nxReadAll = async () => { try { await fetch(`${API}/notifications/read-all?product=${CFG.product}`, { method: 'PUT', headers: _headers() }); document.querySelectorAll('.nx-notif.unread').forEach(el => el.classList.remove('unread')); pollNotifications(); } catch(e) {} }; // ==================== UTILS ==================== function getContext() { return { pageUrl: window.location.href, userName: CFG.userName || (typeof api !== 'undefined' && api.user?.name) || '', userEmail: CFG.userEmail || (typeof api !== 'undefined' && api.user?.email) || '', userRole: CFG.userRole || (typeof api !== 'undefined' && api.user?.role) || '', companyName: (typeof api !== 'undefined' && api.companies?.[0]?.name) || '' }; } function timeAgo(dateStr) { const diff = (Date.now() - new Date(dateStr).getTime()) / 1000; if (diff < 60) return 'ora'; if (diff < 3600) return Math.floor(diff / 60) + ' min fa'; if (diff < 86400) return Math.floor(diff / 3600) + ' ore fa'; return Math.floor(diff / 86400) + ' giorni fa'; } // ==================== MAINTENANCE CHECK (polling maintenance.json) ==================== var _maintOverlay = null; function checkMaintenance() { fetch('/maintenance.json?_=' + Date.now()).then(function(r) { return r.json(); }).then(function(d) { if (d && d.active === true) { if (!_maintOverlay) { _maintOverlay = document.createElement('div'); _maintOverlay.id = 'agile-maint-overlay'; _maintOverlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(10,11,15,0.95);display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; _maintOverlay.innerHTML = '
' + '
๐Ÿ”ง
' + '

Aggiornamento in corso

' + '

Stiamo applicando un miglioramento al sistema. Torneremo operativi tra pochi istanti.

' + '
' + '' + '
'; document.body.appendChild(_maintOverlay); } } else { if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; } } }).catch(function() { if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; } }); } setInterval(checkMaintenance, 10000); setTimeout(checkMaintenance, 3000); // ==================== START ==================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();