- public/guida.html, index-en.html, service-continuity.html - public/js/ai-assistant.js, bug-reporter.js (FAB supporto) - public/mobile-conversion.css/js - index.html, common.js, help.js, risks.html: aggiornamenti UI/help Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
871 lines
38 KiB
JavaScript
871 lines
38 KiB
JavaScript
/**
|
|
* 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:
|
|
* <script src="https://agilehub.agile.software/widgets/bug-reporter.js"
|
|
* data-product="TRPG"
|
|
* data-tenant-id="1"
|
|
* data-api-url="https://agilehub.agile.software"
|
|
* data-user-name="Marco Consulente"
|
|
* data-user-email="marco@studio.it"
|
|
* data-user-role="consultant">
|
|
* </script>
|
|
*/
|
|
(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 = `
|
|
<button id="nx-bell-btn" title="Notifiche"><i class="fas fa-bell"></i><span id="nx-bell-badge" style="display:none">0</span></button>
|
|
<button id="nx-bug-btn" title="Chiedi supporto"><i class="fas fa-life-ring"></i></button>
|
|
`;
|
|
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 = `
|
|
<div id="nx-modal">
|
|
<div class="nx-header">
|
|
<h3 id="nx-title">Segnalazioni</h3>
|
|
<button class="nx-close" onclick="document.getElementById('nx-modal-bg')?.remove();var f=document.getElementById('ai-chat-fab');if(f)f.style.display='flex'">✕</button>
|
|
</div>
|
|
<div class="nx-tabs">
|
|
<button class="nx-tab" data-tab="new">Nuova</button>
|
|
<button class="nx-tab" data-tab="mine">Le Mie</button>
|
|
<button class="nx-tab" data-tab="done">Risolte</button>
|
|
<button class="nx-tab" data-tab="notif">Notifiche</button>
|
|
</div>
|
|
<div class="nx-body" id="nx-body"></div>
|
|
</div>
|
|
`;
|
|
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 = '<div style="text-align:center;padding:30px;color:#9ca3af">Avvio assistente...</div>';
|
|
|
|
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 = `
|
|
<div class="nx-chat-area" id="nx-chat-area"></div>
|
|
<div class="nx-chat-actions">
|
|
<button class="resolve" onclick="window._nxChatResolve()">✅ Risolto, grazie!</button>
|
|
<button class="classic" onclick="window._nxChatSkip()">📝 Segnalazione classica</button>
|
|
</div>
|
|
<div class="nx-chat-input">
|
|
<input type="text" id="nx-chat-input" placeholder="Scrivi il tuo problema..." autofocus>
|
|
<button id="nx-chat-send" onclick="window._nxChatSend()">➤</button>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = '<span></span><span></span><span></span>';
|
|
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 = `
|
|
<div class="nx-ctx">📍 ${ctx.pageUrl.split('#')[1] || ctx.pageUrl.split('/').pop()} ${ctx.companyName ? '| 🏢 ' + ctx.companyName : ''}</div>
|
|
<div class="nx-row">
|
|
<select class="nx-select" id="nx-tipo" style="flex:1">
|
|
<option value="COMPLAINT">🐛 Bug / Errore</option>
|
|
<option value="SUPPORT_REQUEST">🎨 Interfaccia</option>
|
|
<option value="INFO_REQUEST">💡 Suggerimento</option>
|
|
<option value="OTHER">❓ Domanda</option>
|
|
</select>
|
|
<select class="nx-select" id="nx-prio" style="flex:1">
|
|
<option value="MEDIUM">Media</option>
|
|
<option value="LOW">Bassa</option>
|
|
<option value="HIGH">Alta</option>
|
|
<option value="URGENT">Urgente</option>
|
|
</select>
|
|
</div>
|
|
<textarea class="nx-textarea" id="nx-desc" placeholder="Descrivi il problema... (puoi incollare screenshot con Ctrl+V)"></textarea>
|
|
<div class="nx-att-list" id="nx-att-list"></div>
|
|
<div class="nx-row">
|
|
<button class="nx-btn" id="nx-voice-btn" onclick="window._nxVoice()">🎤 Parla</button>
|
|
<button class="nx-btn" id="nx-screenshot-btn" onclick="window._nxScreenshot()">📸 Screenshot</button>
|
|
<label class="nx-btn" style="cursor:pointer">
|
|
📎 Allega
|
|
<input type="file" accept="image/*,.pdf" multiple style="display:none" onchange="window._nxAttachFiles(this.files)">
|
|
</label>
|
|
</div>
|
|
<button class="nx-btn nx-btn-primary" id="nx-submit" onclick="window._nxSubmit()">Invia Segnalazione</button>
|
|
<div id="nx-result" style="display:none"></div>
|
|
`;
|
|
|
|
// 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) => `
|
|
<div class="nx-att-item">
|
|
<img src="${a.preview}" alt="${a.filename}">
|
|
<button class="nx-att-remove" onclick="window._nxRemoveAtt(${i})">✕</button>
|
|
</div>
|
|
`).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!<br><small>${data.data.ticket.aiResponse || 'Grazie, il team la prendera in carico.'}</small>`;
|
|
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 = '<div class="nx-empty">Caricamento...</div>';
|
|
const email = CFG.userEmail;
|
|
if (!email) { body.innerHTML = '<div class="nx-empty">Email utente non configurata</div>'; 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 = `<div class="nx-empty">${statusFilter === 'DONE' ? 'Nessuna segnalazione risolta' : 'Nessuna segnalazione aperta'}</div>`;
|
|
return;
|
|
}
|
|
|
|
body.innerHTML = data.data.tickets.map(t => `
|
|
<div class="nx-ticket" onclick="window._nxToggleTicket(this)">
|
|
<div class="nx-ticket-head">
|
|
<span class="nx-ticket-id">#${t.id}</span>
|
|
<span class="nx-ticket-status nx-status-${t.status}">${t.status.replace('_', ' ')}</span>
|
|
</div>
|
|
<div class="nx-ticket-subject">${t.subject || t.operativeSummary?.substring(0, 80) || 'Senza oggetto'}</div>
|
|
<div class="nx-ticket-meta">${t.priority} | ${timeAgo(t.createdAt)}</div>
|
|
${t.aiResponse ? `<div class="nx-ticket-ai" style="display:none">💬 ${t.aiResponse}</div>` : ''}
|
|
${t.messages?.length ? `<div class="nx-ticket-ai" style="display:none">📝 ${t.messages.map(m => `<b>${m.role}</b>: ${m.content}`).join('<br>')}</div>` : ''}
|
|
${t.status === 'RESOLVED' ? `<button class="nx-reopen-btn" onclick="event.stopPropagation(); window._nxReopenTicket(${t.id}, this)" style="margin-top:8px;width:100%;padding:8px;border:1px solid #ef4444;border-radius:8px;background:#fef2f2;color:#dc2626;font-size:13px;font-weight:600;cursor:pointer">Non risolto — Riapri</button>` : ''}
|
|
</div>
|
|
`).join('');
|
|
} catch(err) {
|
|
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
|
|
}
|
|
}
|
|
|
|
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 = '<div class="nx-empty">Caricamento...</div>';
|
|
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 = '<div class="nx-empty">Nessuna notifica</div>';
|
|
return;
|
|
}
|
|
|
|
let html = `<div style="text-align:right;margin-bottom:8px">
|
|
<button class="nx-btn" style="font-size:12px;padding:4px 10px" onclick="window._nxReadAll()">Segna tutte lette</button>
|
|
</div>`;
|
|
|
|
html += data.data.notifications.map(n => `
|
|
<div class="nx-notif ${n.readAt ? '' : 'unread'}" onclick="window._nxReadNotif(${n.id}, this)">
|
|
<div class="nx-notif-title"><span class="nx-notif-dot ${n.type}"></span>${n.title}</div>
|
|
${n.body ? `<div class="nx-notif-body">${n.body}</div>` : ''}
|
|
<div class="nx-notif-meta">${n.type} | ${timeAgo(n.createdAt)}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
body.innerHTML = html;
|
|
} catch(err) {
|
|
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
|
|
}
|
|
}
|
|
|
|
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 = '<div style="text-align:center;max-width:400px;padding:32px">'
|
|
+ '<div style="font-size:48px;margin-bottom:16px">🔧</div>'
|
|
+ '<h2 style="color:#fff;font-size:22px;font-weight:700;margin:0 0 12px">Aggiornamento in corso</h2>'
|
|
+ '<p style="color:#9ca3af;font-size:15px;line-height:1.6;margin:0 0 20px">Stiamo applicando un miglioramento al sistema. Torneremo operativi tra pochi istanti.</p>'
|
|
+ '<div style="width:48px;height:48px;border:3px solid rgba(124,58,237,0.3);border-top-color:#7c3aed;border-radius:50%;margin:0 auto;animation:agile-spin 1s linear infinite"></div>'
|
|
+ '<style>@keyframes agile-spin{to{transform:rotate(360deg)}}</style>'
|
|
+ '</div>';
|
|
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();
|
|
}
|
|
})();
|