@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
JavaScript
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;
">×</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)
}
}