Conversaciones
Selecciona un chat
0:00 Grabando audio... suelta el botón para enviar
archivo.jpg
Cliente
Gestión
Etapa CRM
Etiquetas
Bitácora privada
Analytics ERP
Rendimiento en tiempo real
Chats Totales
Resueltos
Tiempo Respuesta
Agentes Online
Distribución por Canal
Volumen de mensajes
Rendimiento por Agente
Chats resueltos
Actividad de los últimos 7 días
Mensajes entrantes
Operación en Vivo
En Espera
0
Online
0
En Curso
0
Resueltos Hoy
0
ClienteCanalEsperaPrioridadAcción
Carga por Agente
AgenteEstadoChats Abiertos
Embudo de Ventas
Directorio de Contactos
Gestiona y almacena toda tu base de clientes
Nombre Teléfono Email Empresa Estado Acciones
Campañas de Marketing
Enviados Totales
0
Entregados
0
Leídos (Apertura)
0
Tasa de Apertura
0%
Embudo de Conversión
Rendimiento de tus mensajes masivos
Interacción Real
Leídos vs No leídos
Biblioteca de Medios
Gestiona tus imágenes y videos para usarlos en chats y campañas
Chatbots & Automatización
Gestiona tus bots de respuesta automática

Mis Chatbots Configurados

Respuestas Rápidas
Usa / en el chat para activarlas
AtajoContenidoCategoríaAcciones
Equipo de Asesores
NombreEmailRolEstadoAcciones
Conectividad & API
Vincula tus canales oficiales para recibir mensajes en el buzón unificado
Webhook Único de Recepción
Esta URL recibe los datos de Meta (WA, FB, IG) y Telegram simultáneamente
https://multiagente.turnoagil.com/webhook
Configuración del Sistema
Bot de bienvenida activo
Responde automáticamente mensajes nuevos
Auto-asignación de chats
Distribuye chats automáticamente entre agentes
Habilitar cámara en el chat
Permite capturar y enviar fotos desde el chat
Notificaciones
`; break; } openModal(title, `
${fields}
`, ` `); } async function saveIntegration(platform) { const statusDiv = document.getElementById('config-status'); statusDiv.innerHTML = 'Validando conexión...'; // Aquí recolectamos los datos (el objeto payload dependerá de los IDs definidos arriba) const payload = { platform, token: document.getElementById('integ-token')?.value, phoneId: document.getElementById('integ-phone-id')?.value, wabaId: document.getElementById('integ-waba-id')?.value, verifyToken: document.getElementById('integ-verify')?.value, pageId: document.getElementById('integ-page-id')?.value, domain: document.getElementById('integ-domain')?.value }; try { const res = await apiFetch('/api/integrations/save', { method: 'POST', body: JSON.stringify(payload) }); if (res.ok) { statusDiv.innerHTML = '✅ ¡Conectado con éxito!'; setTimeout(() => { closeModal(); loadIntegrations(); }, 1500); } else { statusDiv.innerHTML = '❌ Error en las llaves ingresadas.'; } } catch(e) { statusDiv.innerHTML = '❌ Error de servidor.'; } } // ══════════════════════════════════════════════════════════════ // SETTINGS // ══════════════════════════════════════════════════════════════ async function loadSettings(){ try { const res=await apiFetch('/api/settings'); const data=await res.json(); if(data){ document.getElementById('config-open').value = data.open_time ||''; document.getElementById('config-close').value = data.close_time ||''; document.getElementById('config-msg').value = data.away_message ||''; document.getElementById('config-welcome').value = data.welcome_msg ||''; if(data.bot_enabled) document.getElementById('toggle-bot').classList.add('on'); if(data.auto_assign) document.getElementById('toggle-autoassign').classList.add('on'); // Restaurar preferencia de cámara const camEnabled = data.camera_enabled || localStorage.getItem('cameraEnabled')==='true'; if(camEnabled){ document.getElementById('toggle-camera').classList.add('on'); cameraEnabled=true; const camBtn=document.getElementById('btn-camera'); if(camBtn) camBtn.style.display='flex'; } } } catch(e){} } async function saveSettings(){ const data={ open_time: document.getElementById('config-open').value, close_time: document.getElementById('config-close').value, away_message: document.getElementById('config-msg').value, welcome_msg: document.getElementById('config-welcome').value, bot_enabled: document.getElementById('toggle-bot').classList.contains('on'), auto_assign: document.getElementById('toggle-autoassign').classList.contains('on'), camera_enabled: document.getElementById('toggle-camera').classList.contains('on') }; // Actualizar botón cámara en tiempo real al guardar cameraEnabled = data.camera_enabled; const camBtn = document.getElementById('btn-camera'); if (camBtn) camBtn.style.display = cameraEnabled ? 'flex' : 'none'; localStorage.setItem('cameraEnabled', cameraEnabled); const res=await apiFetch('/api/settings',{method:'POST',body:JSON.stringify(data)}); if(res.ok) alert('✅ Configuración guardada'); } // ══════════════════════════════════════════════════════════════ // NOTIFICACIONES // ══════════════════════════════════════════════════════════════ function toggleNotifPanel(){ document.getElementById('notif-panel').classList.toggle('open'); loadNotifications(); } async function loadNotifications(){ try { const res=await apiFetch('/api/notifications'); const data=await res.json(); const count=data.filter(n=>!n.read).length; const countEl=document.getElementById('notif-count'); if(count>0){countEl.style.display='flex';countEl.textContent=count>9?'9+':count;} else countEl.style.display='none'; document.getElementById('notif-list').innerHTML=data.length ?data.map(n=>`
${n.message}
${formatTime(n.created_at)}
`).join('') :'
No hay notificaciones
'; } catch(e){} } async function handleNotifClick(id,contactId){ await apiFetch(`/api/notifications/${id}/read`,{method:'PATCH'}); document.getElementById('notif-panel').classList.remove('open'); if(contactId){showView('v-chats',document.getElementById('btn-nav-chats'));setTimeout(()=>selectChat(contactId,'','whatsapp',''),100);} loadNotifications(); } async function markAllNotifsRead(){ await apiFetch('/api/notifications/read-all',{method:'PATCH'}); loadNotifications(); } // ══════════════════════════════════════════════════════════════ // MODALES // ══════════════════════════════════════════════════════════════ function openModal(title,bodyHTML,footerHTML){ document.getElementById('modal-title').innerText=title; document.getElementById('modal-body').innerHTML=bodyHTML; document.getElementById('modal-footer').innerHTML=footerHTML; document.getElementById('modal-overlay').classList.add('open'); } function closeModal(e){ if(!e||e.target===document.getElementById('modal-overlay')) document.getElementById('modal-overlay').classList.remove('open'); } function openQRModal(){ openModal('Nueva Respuesta Rápida',`
`, ` `); } async function createQR(){ const shortcut=document.getElementById('m-shortcut').value.trim(); const content=document.getElementById('m-content').value.trim(); const category=document.getElementById('m-category').value.trim(); if(!shortcut||!content) return alert('Shortcut y contenido son requeridos.'); const res=await apiFetch('/api/quick-replies',{method:'POST',body:JSON.stringify({shortcut,content,category})}); if(res.ok){closeModal();loadQuickRepliesView();loadQuickRepliesForChat();alert('✅ Respuesta rápida creada');} else{const e=await res.json();alert(e.error||'Error');} } function openAgentModal(){ openModal('Crear Asesor',`
`, ` `); } async function createAgent(){ const name=document.getElementById('m-ag-name').value.trim(); const email=document.getElementById('m-ag-email').value.trim(); const password=document.getElementById('m-ag-pass').value.trim(); const role=document.getElementById('m-ag-role').value; if(!name||!email||!password) return alert('Todos los campos son requeridos.'); const res=await apiFetch('/api/agents',{method:'POST',body:JSON.stringify({name,email,password,role})}); if(res.ok){closeModal();loadAgents();} else{const e=await res.json();alert(e.error||'Error');} } function openTagsModal(){ openModal('Gestionar Etiquetas del Contacto',`
`, ` `); loadTagsForModal(); } let currentContactTags=[]; async function loadTagsForModal(){ if(!currentChatId) return; const res=await apiFetch(`/api/contacts/${currentChatId}`); const data=await res.json(); currentContactTags=data.tags||[]; renderModalTags(); } function renderModalTags(){ const el=document.getElementById('m-tags-list'); if(!el) return; el.innerHTML=currentContactTags.map((t,i)=>` ${t}× `).join('')||'Sin etiquetas'; } function addTagToContact(){ const input=document.getElementById('m-new-tag'); const tag=input.value.trim(); if(tag&&!currentContactTags.includes(tag)){currentContactTags.push(tag);input.value='';renderModalTags();} } function removeTagFromList(idx){currentContactTags.splice(idx,1);renderModalTags();} async function saveContactTags(){ if(!currentChatId) return; await apiFetch(`/api/contacts/${currentChatId}/tags`,{method:'PUT',body:JSON.stringify({tags:currentContactTags})}); renderContactTags(currentContactTags); closeModal(); } function openTransferModal(){ openModal('Transferir Chat',`
`, ` `); apiFetch('/api/agents').then(r=>r.json()).then(agents=>{ document.getElementById('m-transfer-agent').innerHTML=agents.map(a=>``).join(''); }); } async function doTransfer(){ const to_agent_id=document.getElementById('m-transfer-agent').value; const reason=document.getElementById('m-transfer-reason').value; if(!to_agent_id) return alert('Selecciona un agente.'); await apiFetch(`/api/contacts/${currentChatId}/transfer`,{method:'POST',body:JSON.stringify({to_agent_id,reason})}); closeModal(); alert('✅ Chat transferido'); loadContacts(); } function openAssignModal(contactId){ openModal('Asignar Chat',`
`, ` `); apiFetch('/api/agents').then(r=>r.json()).then(agents=>{ document.getElementById('m-assign-agent').innerHTML=agents.filter(a=>a.status==='Online').map(a=>``).join(''); }); } async function doAssign(contactId){ const agent_id=document.getElementById('m-assign-agent').value; await apiFetch('/api/supervisor/assign',{method:'POST',body:JSON.stringify({contact_id:contactId,agent_id})}); closeModal(); loadSupervisor(); loadContacts(); } function openMediaGallery(){ if(!currentChatId) return; openModal('Archivos Multimedia','', ``); apiFetch(`/api/media/${currentChatId}`).then(r=>r.json()).then(files=>{ const el=document.getElementById('m-media-gallery'); if(!files.length){el.innerHTML='
No hay archivos
';return;} el.innerHTML=files.map(f=>{ const parts=f.body?.split('|')||[]; const url=parts[2]||f.media_url; const type=f.msg_type; if(type==='image') return ``; return `📄 ${type}`; }).join(''); }); } function openQuickRepliesModal(){showView('v-quickreplies',document.getElementById('btn-nav-quickreplies'));closeModal();} // ══════════════════════════════════════════════════════════════ // NOTIFICACIONES DEL NAVEGADOR // ══════════════════════════════════════════════════════════════ function requestNotifPermission(){ if('Notification'in window&&Notification.permission==='default') Notification.requestPermission(); } function showBrowserNotif(title,body,chatId){ if(Notification.permission==='granted'){ const n=new Notification(title,{body,icon:'/favicon.ico',tag:`chat-${chatId}`}); n.onclick=()=>{window.focus();if(chatId) selectChat(chatId,title,'whatsapp','');}; } } function playNotifSound(){ const ctx=new(window.AudioContext||window.webkitAudioContext)(); const osc=ctx.createOscillator(); const gain=ctx.createGain(); osc.connect(gain);gain.connect(ctx.destination); osc.frequency.value=520;osc.type='sine'; gain.gain.setValueAtTime(0,ctx.currentTime); gain.gain.linearRampToValueAtTime(0.3,ctx.currentTime+0.01); gain.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+0.3); osc.start(ctx.currentTime);osc.stop(ctx.currentTime+0.3); } // ══════════════════════════════════════════════════════════════ // SOCKET.IO // ══════════════════════════════════════════════════════════════ socket.on('new_message',(data)=>{ if(data.direction==='inbound'){ const badge=document.getElementById('chat-badge'); if(badge) badge.style.display='block'; playNotifSound(); showBrowserNotif(`Nuevo mensaje de ${data.name||data.from||'Cliente'}`,data.text?.substring(0,60)||'',data.chatId); loadNotifications(); } if(String(data.chatId)===String(currentChatId)){ loadChatHistory(currentChatId,false); apiFetch(`/api/messages/${currentChatId}/read`,{method:'PATCH'}).catch(()=>{}); } loadContacts(); }); socket.on('message_updated',(data)=>{ if(String(data.chatId)===String(currentChatId)) loadChatHistory(currentChatId,false); loadContacts(); const supView=document.getElementById('v-supervisor'); if(supView&&supView.classList.contains('active')) loadSupervisor(); }); socket.on('typing_start',(data)=>{ const el=document.getElementById('typing-indicator'); if(el){el.style.display='block';el.textContent=`✍️ ${data.agentName||'Agente'} está escribiendo...`;} }); socket.on('typing_stop',()=>{ const el=document.getElementById('typing-indicator'); if(el) el.style.display='none'; }); socket.on('notification',(data)=>{ playNotifSound(); showBrowserNotif('Turno Ágil',data.message,data.chatId); loadNotifications(); }); // ══════════════════════════════════════════════════════════════ // UTILS // ══════════════════════════════════════════════════════════════ function esc(s){ return (s||'').replace(/'/g,"\\'").replace(/"/g,'"'); } function escHtml(s){ return (s||'').replace(/&/g,'&').replace(//g,'>'); } // ══════════════════════════════════════════════════════════════ // BUSCADOR DE CONTACTOS // ══════════════════════════════════════════════════════════════ const searchInputEl=document.querySelector('input[placeholder="Buscar contacto..."]'); if(searchInputEl){ searchInputEl.addEventListener('input',(e)=>{ const term=e.target.value.toLowerCase().trim(); const activeTab=document.querySelector('.cp-tab.active'); let type='all'; if(activeTab&&activeTab.textContent.includes('Espera')) type='pending'; if(activeTab&&activeTab.textContent.includes('Abierto')) type='open'; document.querySelectorAll('.contact-item').forEach(item=>{ const name=item.querySelector('.ci-name').textContent.toLowerCase(); const phone=(item.dataset.phone||'').toLowerCase(); const status=item.dataset.status; const matchesTab=type==='all'||(type==='pending'&&status==='Pendiente')||(type==='open'&&status==='Abierto'); const matchesSearch=name.includes(term)||phone.includes(term); item.style.display=(matchesTab&&matchesSearch)?'flex':'none'; }); }); } // ══════════════════════════════════════════════════════════════ // 📁 LÓGICA DE LA BIBLIOTECA DE MEDIOS // ══════════════════════════════════════════════════════════════ // 1. Mostrar la vista al hacer clic en el menú const originalShowView = showView; showView = function(viewId, btn) { if (typeof originalShowView === 'function') originalShowView(viewId, btn); if(viewId === 'v-library') loadLibrary(); }; // 2. Cargar los archivos desde el servidor async function loadLibrary() { const grid = document.getElementById('library-grid'); if (!grid) return; grid.innerHTML = '
Cargando medios...
'; try { const res = await apiFetch('/api/library'); const data = await res.json(); if(data.length === 0) { grid.innerHTML = '
No hay archivos en la biblioteca.
'; return; } grid.innerHTML = data.map(m => `
${m.filename}
`).join(''); } catch(e) { grid.innerHTML = '
Error cargando biblioteca.
'; } } // 3. Subir archivo async function uploadToLibrary(input) { const file = input.files[0]; if(!file) return; const fd = new FormData(); fd.append('file', file); try { const res = await fetch('/api/library/upload', { method:'POST', headers:{'Authorization':`Bearer ${TOKEN}`}, body:fd }); if(res.ok) { alert('✅ Archivo guardado en la biblioteca'); loadLibrary(); } else { alert('❌ Error al subir archivo'); } } catch(e) { alert('❌ Error de conexión al subir'); } input.value = ''; } // 4. Eliminar archivo async function deleteFromLibrary(id) { if(!confirm('¿Seguro que deseas borrar este archivo?')) return; await apiFetch(`/api/library/${id}`, { method:'DELETE' }); loadLibrary(); } // 5. Copiar URL function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => alert('URL copiada: ' + text)); } // 6. Abrir Modal de Selección (Para las Campañas) async function showLibraryModal(target) { openModal('Selecciona un archivo', '
Cargando...
', ''); try { const res = await apiFetch('/api/library'); const data = await res.json(); const bodyHTML = `
${data.map(m => `
`).join('')}
`; document.getElementById('modal-body').innerHTML = bodyHTML; } catch(e) { document.getElementById('modal-body').innerHTML = 'Error al cargar'; } } function selectLibraryItem(url, target) { if(target === 'campaign') { closeModal(); setTimeout(() => { openCampaignModal(); setTimeout(() => { const input = document.getElementById('m-camp-media-url'); if(input) input.value = url; }, 100); }, 200); } } // ══════════════════════════════════════════════════════════════ // 📊 ESTADÍSTICAS Y EXPORTACIÓN EXCEL // ══════════════════════════════════════════════════════════════ let botsChartInstance = null; function exportToCSV(filename, rows) { if (!rows || !rows.length) { alert("No hay datos para exportar"); return; } const separator = ';'; const keys = Object.keys(rows[0]); const csvContent = keys.join(separator) + '\n' + rows.map(row => keys.map(k => { let cell = row[k] === null || row[k] === undefined ? '' : row[k]; cell = cell.toString().replace(/"/g, '""'); if (cell.search(/("|,|\n|;)/g) >= 0) cell = `"${cell}"`; return cell; }).join(separator)).join('\n'); const blob = new Blob(["\ufeff", csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); link.setAttribute("href", URL.createObjectURL(blob)); link.setAttribute("download", filename + ".csv"); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } async function loadBotStats() { try { const response = await fetch('/api/bots/stats/dashboard', { headers: { 'Authorization': `Bearer ${TOKEN}` } }); if (!response.ok) return; const data = await response.json(); const container = document.getElementById('bot-stats-container'); if (container) container.style.display = 'block'; const fields = { 'stat-total-interactions': data.cards.totalInteractions, 'stat-bot-messages': data.cards.botMessagesSent, 'stat-active-bots': data.cards.activeBots, 'stat-total-bots': data.cards.totalBots }; for (const [id, val] of Object.entries(fields)) { const el = document.getElementById(id); if (el) el.innerText = val || 0; } const canvas = document.getElementById('botsPerformanceChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const labels = data.botsPerformance.map(b => b.name); const interactions = data.botsPerformance.map(b => b.interactions); if (botsChartInstance) { botsChartInstance.data.labels = labels; botsChartInstance.data.datasets[0].data = interactions; botsChartInstance.update(); } else { botsChartInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Interacciones', data: interactions, backgroundColor: '#6366f1' }] }, options: { responsive: true, maintainAspectRatio: false } }); } } catch (e) { console.error("Error stats:", e); } } async function downloadExcel(type) { try { const url = type === 'bots' ? '/api/bots/stats/dashboard' : '/api/campaigns'; const res = await apiFetch(url); const data = await res.json(); const rows = type === 'bots' ? data.botsPerformance : data; exportToCSV(type === 'bots' ? "Reporte_Bots" : "Reporte_Campanas", rows); } catch (e) { alert("Error al descargar Excel"); } } // ══════════════════════════════════════════════════════════════ // 🚀 INICIALIZACIÓN (INIT) // ══════════════════════════════════════════════════════════════ window.onload = async () => { // Verificar Token if (!TOKEN) { window.location.href = '/login.html'; return; } // Cargas iniciales applyPermissions(); requestNotifPermission(); await loadContacts(); await loadAgents(); await loadSettings(); await loadQuickRepliesForChat(); loadNotifications(); // Intervalos setInterval(loadNotifications, 30000); setInterval(loadContacts, 60000); // Dashboard inicial loadBotStats(); };