/**
* 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 = `
`;
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) => `
`).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.aiResponse}
` : ''}
${t.messages?.length ? `
๐ ${t.messages.map(m => `${m.role}: ${m.content}`).join('
')}
` : ''}
${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();
}
})();