TA
Turno Ágil
ERP Platform
Principal
Conversaciones
Torre de Control
Analytics
CRM Pipeline
Directorio CRM
Herramientas
Chatbots IA
Campañas
Biblioteca Medios
Equipo
Resp. Rápidas
Sistema
Integraciones
Configuración
Notificaciones
U
Asesor
agente
Conversaciones
Todos
Espera
Abiertos
Selecciona un chat
—
✍️ escribiendo...
0:00
Grabando audio... suelta el botón para enviar
Cancelar
Cerrar cámara
📷 Capturar foto
archivo.jpg
Adjuntar
Plantillas
Respuesta IA
Cliente
—
● Pendiente
Gestión
Sin asignar
Sin clasificar
💬 Asesoría
💰 Venta
🔧 Soporte
🔴 Pendiente
🟡 En Curso
🟢 Resuelto
Etapa CRM
🆕 Prospecto
📞 Contactado
🤝 Negociación
✅ Cerrado
Etiquetas
Gestionar etiquetas
Bitácora privada
Guardar cambios
Analytics ERP
Rendimiento en tiempo real
Auditoría de Chats
Exportar Reporte
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
Cliente
Canal
Espera
Prioridad
Acción
Carga por Agente
Agente
Estado
Chats Abiertos
Embudo de Ventas
Directorio de Contactos
Gestiona y almacena toda tu base de clientes
Importar
Exportar
Nuevo Contacto
Nombre
Teléfono
Email
Empresa
Estado
Acciones
Campañas de Marketing
Exportar Excel
Nueva Campaña
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
Subir Archivo
Chatbots & Automatización
Gestiona tus bots de respuesta automática
Exportar Excel
Nuevo Bot
Mis Chatbots Configurados
Estadísticas de Automatización
Interacciones Totales
—
Mensajes del Bot
—
Bots Activos
—
Total de Bots
—
Rendimiento por Bot
Carga de Trabajo
Respuestas Rápidas
Usa / en el chat para activarlas
Nueva Plantilla
Atajo
Contenido
Categoría
Acciones
Equipo de Asesores
Crear Asesor
Nombre
Email
Rol
Estado
Acciones
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
Copiar
Configuración del Sistema
Hora de Apertura
Hora de Cierre
Mensaje de Bienvenida (Bot)
Mensaje de Ausencia
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
Actualizar Parámetros
Notificaciones
Marcar todas leídas
Modal
`; break; } openModal(title, `
${fields}
`, `
Cancelar
Validar y Guardar
`); } 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',`
Atajo (comienza con /)
Contenido
Categoría
`, `
Cancelar
Guardar
`); } 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',`
Nombre
Email
Contraseña
Rol
Agente
Supervisor
Admin
`, `
Cancelar
Crear
`); } 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',`
Agregar
`, `
Cancelar
Guardar
`); 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',`
Transferir a
Selecciona un agente...
Motivo (opcional)
`, `
Cancelar
Transferir
`); apiFetch('/api/agents').then(r=>r.json()).then(agents=>{ document.getElementById('m-transfer-agent').innerHTML=agents.map(a=>`
${a.name}
`).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',`
Asignar a
`, `
Cancelar
Asignar
`); apiFetch('/api/agents').then(r=>r.json()).then(agents=>{ document.getElementById('m-assign-agent').innerHTML=agents.filter(a=>a.status==='Online').map(a=>`
${a.name}
`).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','
Cargando...
', `
Cerrar
`); 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...
', '
Cerrar
'); 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(); };