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