UNPKG

@deeprocket/deep-service-desk-widget

Version:

Widget Vue.js para integração com o sistema Deep Service Desk - Controle de visibilidade por URLs - Compatível com Vue 2.7.16 e Vue 3 - Com botão flutuante automático

825 lines (720 loc) 26.9 kB
import TicketWidget from './components/TicketWidget.vue' import FloatingTicketWidget from './components/FloatingTicketWidget.vue' // Variável global para armazenar a configuração let globalConfig = null // Variável para controlar se o monitoramento já foi configurado let urlMonitoringConfigured = false // Função global para forçar verificação de visibilidade (pode ser chamada externamente) window.deepServiceDeskUpdateVisibility = function() { if (globalConfig) { updateButtonVisibility(globalConfig) } } // 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 verificar se a URL atual deve ocultar o botão function shouldHideButtonOnCurrentUrl(hiddenUrls) { if (!hiddenUrls || !Array.isArray(hiddenUrls) || hiddenUrls.length === 0) { return false } const currentUrl = window.location.href const currentPath = window.location.pathname return hiddenUrls.some(urlPattern => { // Se é uma string simples, verificar se é substring da URL ou path if (typeof urlPattern === 'string') { return currentUrl.includes(urlPattern) || currentPath.includes(urlPattern) } // Se é uma expressão regular if (urlPattern instanceof RegExp) { return urlPattern.test(currentUrl) || urlPattern.test(currentPath) } return false }) } // 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')) { // Se o botão já existe, apenas atualizar a visibilidade updateButtonVisibility(config) return } // SEMPRE criar o botão, mas definir visibilidade inicial baseada na URL const shouldHideInitially = shouldHideButtonOnCurrentUrl(config.hiddenUrls) // 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 (incluindo visibilidade inicial) const brandColor = config.brandColor || '#3b82f6' button.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: ${brandColor}; border: none; border-radius: 50%; color: white; cursor: pointer; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); z-index: 9998; display: ${shouldHideInitially ? 'none' : '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) // SEMPRE configurar monitoramento de URL (não apenas quando há hiddenUrls) setupUrlMonitoring(config, button) } // Função para atualizar visibilidade do botão function updateButtonVisibility(config) { const button = document.getElementById('deep-service-desk-floating-btn') if (button) { // SEMPRE verificar - se não há hiddenUrls, o botão deve ser visível const shouldHide = shouldHideButtonOnCurrentUrl(config.hiddenUrls) button.style.display = shouldHide ? 'none' : 'flex' // Debug apenas em localhost if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { console.log('[Deep Service Desk] updateButtonVisibility:') console.log(' URL atual:', window.location.href) console.log(' hiddenUrls:', config.hiddenUrls) console.log(' shouldHide:', shouldHide) console.log(' display:', button.style.display) } } } // Função separada para configurar monitoramento de URL function setupUrlMonitoring(config, button) { // Se não há URLs ocultas, não precisa monitorar if (!config.hiddenUrls || config.hiddenUrls.length === 0) { return } // Se monitoramento já foi configurado, não configurar novamente if (urlMonitoringConfigured) { return } urlMonitoringConfigured = true let lastUrl = window.location.href const checkUrlAndToggleButton = () => { const currentUrl = window.location.href // Só verificar se a URL realmente mudou if (currentUrl !== lastUrl) { lastUrl = currentUrl } // Usar a função unificada para atualizar visibilidade updateButtonVisibility(config) } // Múltiplas estratégias para detectar mudanças de URL // 1. Interceptar pushState e replaceState const originalPushState = history.pushState const originalReplaceState = history.replaceState history.pushState = function() { originalPushState.apply(history, arguments) setTimeout(checkUrlAndToggleButton, 50) setTimeout(checkUrlAndToggleButton, 200) // Fallback adicional } history.replaceState = function() { originalReplaceState.apply(history, arguments) setTimeout(checkUrlAndToggleButton, 50) setTimeout(checkUrlAndToggleButton, 200) // Fallback adicional } // 2. Escutar eventos de navegação window.addEventListener('popstate', () => { setTimeout(checkUrlAndToggleButton, 50) }) window.addEventListener('hashchange', () => { setTimeout(checkUrlAndToggleButton, 50) }) // 3. Observer para mudanças no DOM (pode indicar mudança de página) if (typeof MutationObserver !== 'undefined') { const observer = new MutationObserver(() => { // Verificar se a URL mudou if (window.location.href !== lastUrl) { setTimeout(checkUrlAndToggleButton, 100) } }) observer.observe(document.body, { subtree: true, childList: true }) } // 4. Verificação periódica como fallback (a cada 2 segundos) setInterval(() => { if (window.location.href !== lastUrl) { checkUrlAndToggleButton() } }, 2000) // 5. Escutar eventos customizados que podem indicar navegação window.addEventListener('beforeunload', checkUrlAndToggleButton) window.addEventListener('load', () => { setTimeout(checkUrlAndToggleButton, 100) }) // 6. Detectar frameworks populares e escutar eventos específicos // Vue Router if (window.Vue || document.querySelector('[data-v-]')) { // Escutar eventos do Vue Router window.addEventListener('vue-router-navigation', () => { setTimeout(checkUrlAndToggleButton, 50) }) } // React Router if (window.React || document.querySelector('[data-reactroot]')) { // Escutar mudanças no React const checkReactNavigation = () => { setTimeout(checkUrlAndToggleButton, 50) } window.addEventListener('react-router-navigation', checkReactNavigation) } // 7. Interceptar fetch e XMLHttpRequest para detectar possíveis navegações AJAX const originalFetch = window.fetch if (originalFetch) { window.fetch = function() { const result = originalFetch.apply(this, arguments) result.then(() => { // Verificar após requests AJAX (pode indicar mudança de página) setTimeout(() => { if (window.location.href !== lastUrl) { checkUrlAndToggleButton() } }, 300) }).catch(() => { // Mesmo em caso de erro, verificar setTimeout(() => { if (window.location.href !== lastUrl) { checkUrlAndToggleButton() } }, 300) }) return result } } // Verificação inicial setTimeout(checkUrlAndToggleButton, 100) } // 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 simplificado --> <div class="widget-header" style=" display: flex; justify-content: flex-end; align-items: center; padding: 15px 20px; border-bottom: 1px solid #e5e7eb; background-color: #f9fafb; border-radius: 8px 8px 0 0; "> <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; transition: background-color 0.2s; ">&times;</button> </div> <!-- Formulário de Ticket --> <div class="widget-form" style="padding: 24px;"> <!-- Título e subtítulo dentro do conteúdo --> <div style="text-align: center; margin-bottom: 24px;"> <h3 style="margin: 0 0 8px 0; color: #1f2937; font-size: 20px; font-weight: 600;">Precisa de ajuda?</h3> <p style="margin: 0; color: #6b7280; font-size: 14px;">Fale com um de nossos especialistas</p> </div> <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; transition: border-color 0.2s, box-shadow 0.2s; " 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; transition: border-color 0.2s, box-shadow 0.2s; " 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: ${finalConfig.brandColor || '#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 e estilos dinâmicos --> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Hover effect no botão close */ .widget-close:hover { background-color: #f3f4f6 !important; } /* Focus states com brand color */ #ticket-title:focus, #ticket-description:focus { outline: none !important; border-color: ${finalConfig.brandColor || '#3b82f6'} !important; box-shadow: 0 0 0 3px ${finalConfig.brandColor || '#3b82f6'}20 !important; } </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 - sempre usar URL fixa const response = await fetch('https://servicedesk.deeprocket.com.br/api/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 = { clientUuid: options.clientUuid || null, brandColor: options.brandColor || '#3b82f6', showFloatingButton: options.showFloatingButton !== false, // true por padrão hiddenUrls: options.hiddenUrls || [], // URLs onde o botão não deve aparecer ...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 widget global sempre (independentemente do botão flutuante) const initializeWidget = () => { // Criar botão flutuante apenas se showFloatingButton for true if (config.showFloatingButton) { createFloatingButton(config) } // SEMPRE criar instância global do widget (necessário para openTicket funcionar) 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() { // SEMPRE criar o botão (a função já verifica se existe) createFloatingButton(config) }, hideFloatingButton() { removeFloatingButton() }, openTicket() { window.dispatchEvent(new CustomEvent('new-ticket')) }, updateUrlVisibility() { // Método para verificar manualmente a visibilidade baseada na URL updateButtonVisibility(config) } } 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) } }