UNPKG

@pedrohrs/deep-service-desk-widget

Version:

Widget Vue.js para integração com o sistema Deep Service Desk - Versão limpa sem console.logs - Compatível com Vue 2.7.16 e Vue 3 - Com botão flutuante automático

607 lines (537 loc) 19.2 kB
import TicketWidget from './components/TicketWidget.vue' import FloatingTicketWidget from './components/FloatingTicketWidget.vue' // Variável global para armazenar a configuração let globalConfig = null // Função para detectar a versão do Vue function getVueVersion() { if (typeof window !== 'undefined' && window.Vue) { return window.Vue.version || '2' } // Em ambiente de build, tentar detectar pela estrutura return '3' // Padrão para Vue 3 } // Função para criar o botão flutuante function createFloatingButton(config) { // Verificar se o botão já existe if (document.getElementById('deep-service-desk-floating-btn')) { return } // Criar o botão flutuante const button = document.createElement('button') button.id = 'deep-service-desk-floating-btn' button.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.17L4 17.17V4H20V16Z" fill="currentColor"/> <path d="M7 9H17V11H7V9ZM7 12H17V14H7V12ZM7 6H17V8H7V6Z" fill="currentColor"/> </svg> ` // Estilos do botão button.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 50%; color: white; cursor: pointer; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); z-index: 9998; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; font-size: 0; ` // Efeitos hover button.addEventListener('mouseenter', () => { button.style.transform = 'scale(1.1)' button.style.boxShadow = '0 6px 25px rgba(0, 0, 0, 0.2)' }) button.addEventListener('mouseleave', () => { button.style.transform = 'scale(1)' button.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.15)' }) // Evento de clique button.addEventListener('click', () => { window.dispatchEvent(new CustomEvent('new-ticket')) }) // Adicionar tooltip button.title = 'Abrir Suporte' // Inserir no DOM document.body.appendChild(button) // Adicionar estilos responsivos const style = document.createElement('style') style.textContent = ` @media (max-width: 768px) { #deep-service-desk-floating-btn { bottom: 15px !important; right: 15px !important; width: 50px !important; height: 50px !important; } } ` document.head.appendChild(style) } // Função para remover o botão flutuante function removeFloatingButton() { const button = document.getElementById('deep-service-desk-floating-btn') if (button) { button.remove() } } // Variável global para armazenar a instância do Vue let globalVueInstance = null // Função para criar a instância global do widget function createGlobalWidget(config, isVue3, vueApp) { // Verificar se já existe if (document.getElementById('deep-service-desk-global-widget')) { return } // Criar container para o widget const container = document.createElement('div') container.id = 'deep-service-desk-global-widget' document.body.appendChild(container) // SEMPRE usar o widget independente com formulário completo // Isso garante que o formulário sempre funcione, independente do Vue createIndependentWidget(config) } // Função para criar um widget independente (sem Vue) function createIndependentWidget(config) { // Garantir que temos acesso à configuração global const finalConfig = config || globalConfig if (!finalConfig || !finalConfig.clientUuid) { console.error('❌ ERRO CRÍTICO: Configuração não possui clientUuid!') console.error('❌ Config recebida:', config) console.error('❌ Global config:', globalConfig) return } const container = document.getElementById('deep-service-desk-global-widget') if (!container) return // Criar HTML do widget com formulário completo container.innerHTML = ` <div id="independent-ticket-widget" style="display: none;"> <!-- Overlay do modal --> <div class="widget-overlay" style=" position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; "> <div class="widget-modal" style=" background: white; border-radius: 8px; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); "> <!-- Header --> <div class="widget-header" style=" display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; background-color: #f9fafb; border-radius: 8px 8px 0 0; "> <h3 style="margin: 0; color: #1f2937; font-size: 18px; font-weight: 600;">Novo Ticket de Suporte</h3> <button class="widget-close" style=" background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 4px; ">&times;</button> </div> <!-- Formulário de Ticket --> <div class="widget-form" style="padding: 24px;"> <form id="ticket-form"> <!-- Título --> <div style="margin-bottom: 16px;"> <label style="display: block; margin-bottom: 6px; font-weight: 500; color: #374151; font-size: 14px;"> Título * </label> <input type="text" id="ticket-title" required style=" width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; box-sizing: border-box; " placeholder="Resumo do problema ou solicitação" /> </div> <!-- Descrição --> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 6px; font-weight: 500; color: #374151; font-size: 14px;"> Descrição * </label> <textarea id="ticket-description" required rows="6" style=" width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; box-sizing: border-box; resize: vertical; min-height: 120px; " placeholder="Descreva detalhadamente o problema ou solicitação..." ></textarea> </div> <!-- Loading indicator --> <div id="form-loading" style="display: none; text-align: center; margin: 16px 0;"> <div style=" display: inline-block; width: 20px; height: 20px; border: 2px solid #e5e7eb; border-top: 2px solid #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; "></div> <span style="margin-left: 8px; color: #6b7280; font-size: 14px;">Enviando...</span> </div> <!-- Botões --> <div style=" display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; "> <button type="button" class="widget-close" style=" padding: 10px 20px; border: 1px solid #d1d5db; background-color: white; color: #374151; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; " > Cancelar </button> <button type="submit" id="submit-ticket" style=" padding: 10px 20px; border: none; background-color: #3b82f6; color: white; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; " > Enviar Ticket </button> </div> </form> </div> </div> </div> </div> <!-- CSS para animação de loading --> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> ` // Adicionar event listeners const widget = container.querySelector('#independent-ticket-widget') const overlay = container.querySelector('.widget-overlay') const closeButtons = container.querySelectorAll('.widget-close') const form = container.querySelector('#ticket-form') const loadingDiv = container.querySelector('#form-loading') const submitButton = container.querySelector('#submit-ticket') // Função para fechar o widget const closeWidget = () => { widget.style.display = 'none' // Limpar formulário form.reset() loadingDiv.style.display = 'none' submitButton.disabled = false } // Event listeners para fechar closeButtons.forEach(btn => { btn.addEventListener('click', closeWidget) }) overlay.addEventListener('click', (e) => { if (e.target === overlay) closeWidget() }) // Event listener para envio do formulário form.addEventListener('submit', async (e) => { e.preventDefault() // Mostrar loading loadingDiv.style.display = 'block' submitButton.disabled = true // Garantir que temos a configuração mais atualizada const currentConfig = finalConfig || globalConfig if (!currentConfig || !currentConfig.clientUuid) { console.error('❌ ERRO: Não foi possível obter clientUuid!') console.error('❌ finalConfig:', finalConfig) console.error('❌ globalConfig:', globalConfig) showNotification('Erro: Configuração inválida. UUID do cliente não encontrado.', 'error') loadingDiv.style.display = 'none' submitButton.disabled = false return } // Coletar dados do formulário const formData = { title: document.getElementById('ticket-title').value, description: document.getElementById('ticket-description').value, type: 'suporte', status: 'pendente', priority: 'media', clientUuid: currentConfig.clientUuid } try { // Fazer requisição para a API const response = await fetch(`${currentConfig.apiUrl}/tickets/external`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData) }) // Verificar se a resposta é JSON válida const contentType = response.headers.get('content-type') if (!contentType || !contentType.includes('application/json')) { const textResponse = await response.text() console.error('❌ Resposta não é JSON:', textResponse) throw new Error(`Resposta inválida do servidor: ${textResponse}`) } if (response.ok) { const result = await response.json() // Mostrar notificação de sucesso showNotification('Ticket criado com sucesso! Você receberá uma resposta em breve.', 'success') // Fechar widget closeWidget() // Disparar evento global window.dispatchEvent(new CustomEvent('ticket-created', { detail: result })) } else { const errorText = await response.text() console.error('❌ Erro na resposta:', errorText) // Tentar fazer parse do JSON de erro let errorData try { errorData = JSON.parse(errorText) } catch (e) { errorData = { error: errorText } } throw new Error(`Erro ${response.status}: ${response.statusText} - ${errorData.error || errorText}`) } } catch (error) { console.error('❌ Erro ao criar ticket:', error) console.error('❌ Stack trace:', error.stack) console.error('❌ Dados que causaram erro:', formData) console.error('❌ Config usada:', currentConfig) // Mostrar notificação de erro showNotification(`Erro ao enviar ticket: ${error.message}`, 'error') // Disparar evento de erro window.dispatchEvent(new CustomEvent('ticket-error', { detail: error })) } finally { // Esconder loading loadingDiv.style.display = 'none' submitButton.disabled = false } }) // Listener para o evento new-ticket window.addEventListener('new-ticket', () => { // Sempre mostrar o formulário completo widget.style.display = 'block' // Focar no primeiro campo setTimeout(() => { document.getElementById('ticket-title').focus() }, 100) }) } // Função para mostrar notificações function showNotification(message, type = 'info') { // Criar notificação toast const notification = document.createElement('div') notification.className = `deep-service-desk-notification ${type}` notification.textContent = message notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'}; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); z-index: 10000; font-size: 14px; font-weight: 500; max-width: 300px; word-wrap: break-word; animation: slideInRight 0.3s ease; ` // Adicionar animação CSS se não existir if (!document.getElementById('deep-service-desk-notification-styles')) { const style = document.createElement('style') style.id = 'deep-service-desk-notification-styles' style.textContent = ` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } ` document.head.appendChild(style) } document.body.appendChild(notification) // Remover após 5 segundos setTimeout(() => { notification.style.animation = 'slideOutRight 0.3s ease' setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification) } }, 300) }, 5000) } // Plugin de configuração global compatível com Vue 2 e Vue 3 const DeepServiceDeskPlugin = { install(app, options = {}) { // Configurações globais - CORREÇÃO: garantir que clientUuid não seja null const config = { apiUrl: options.apiUrl || 'http://localhost:3000/api', clientUuid: options.clientUuid || null, showFloatingButton: options.showFloatingButton !== false, // true por padrão ...options } // VALIDAÇÃO CRÍTICA: verificar se clientUuid foi fornecido if (!config.clientUuid) { console.error('❌ ERRO CRÍTICO: clientUuid é obrigatório!') console.error('❌ Configuração recebida:', options) console.error('❌ Configuração processada:', config) throw new Error('Deep Service Desk Widget: clientUuid é obrigatório na configuração') } // Salvar configuração globalmente globalConfig = config // Detectar versão do Vue const isVue3 = app.version && app.version.startsWith('3') if (isVue3) { // Vue 3 app.config.globalProperties.$deepServiceDesk = config app.provide('deepServiceDeskConfig', config) app.component('TicketWidget', TicketWidget) app.component('DeepServiceDeskWidget', TicketWidget) app.component('FloatingTicketWidget', FloatingTicketWidget) } else { // Vue 2.7.16 app.prototype.$deepServiceDesk = config // Para Vue 2, usar um mixin global para disponibilizar a configuração app.mixin({ data() { return { deepServiceDeskConfig: config } }, provide() { return { deepServiceDeskConfig: config } } }) app.component('TicketWidget', TicketWidget) app.component('DeepServiceDeskWidget', TicketWidget) app.component('FloatingTicketWidget', FloatingTicketWidget) } // Criar o botão flutuante e widget global quando o DOM estiver pronto if (config.showFloatingButton) { const initializeWidget = () => { createFloatingButton(config) // Criar instância global do widget COMPLETO setTimeout(() => { createGlobalWidget(config, isVue3, app) }, 100) } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeWidget) } else { initializeWidget() } } // Adicionar métodos globais para controlar o botão const methods = { showFloatingButton() { createFloatingButton(config) }, hideFloatingButton() { removeFloatingButton() }, openTicket() { window.dispatchEvent(new CustomEvent('new-ticket')) } } if (isVue3) { app.config.globalProperties.$deepServiceDeskButton = methods } else { app.prototype.$deepServiceDeskButton = methods } } } // Exportar plugin e componentes export default DeepServiceDeskPlugin export { TicketWidget, FloatingTicketWidget } // Auto-instalação se usado via script tag if (typeof window !== 'undefined' && window.Vue) { const vueVersion = getVueVersion() if (vueVersion.startsWith('2')) { window.Vue.use(DeepServiceDeskPlugin) } }