1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
/**
* useTicketEvents.ts
*
* Hook para escutar os Server-Sent Events (SSE) disparados pelo backend.
* Conecta via EventSource e despacha atualizações e invalidações de cache.
*/
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuth } from '@/context/AuthContext';
import { useToast } from '@/context/ToastContext';
export function useTicketEvents() {
const { token, isAuthenticated, user } = useAuth();
const queryClient = useQueryClient();
const { addToast } = useToast();
useEffect(() => {
// Pede permissão para notificações nativas, se suportado
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
useEffect(() => {
if (!isAuthenticated || !token) return;
// Conecta ao endpoint SSE passando o token na query string
const url = `${import.meta.env['VITE_API_URL'] ?? '/api/v1'}/events?token=${token}`;
const eventSource = new EventSource(url);
const notify = (title: string, body?: string) => {
// Toast notification
addToast(title, 'info');
// Native notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body, icon: '/favicon.ico' });
}
};
// Quando um novo chamado é criado
eventSource.addEventListener('ticket.created', (e) => {
const data = JSON.parse(e.data);
if (user?.role === 'admin' || user?.role === 'agent') {
if (data.creatorId !== user.id) {
notify(`Novo chamado: #${data.id}`, data.title);
}
}
// Invalida a lista para forçar o refetch e atualizar o grid
queryClient.invalidateQueries({ queryKey: ['tickets'] });
queryClient.invalidateQueries({ queryKey: ['stats'] });
});
// Quando o status ou responsável do chamado é atualizado
eventSource.addEventListener('ticket.updated', (e) => {
const data = JSON.parse(e.data);
if (user && (user.id === data.creatorId || user.id === data.assigneeId) && user.id !== data.authorId) {
notify(`Chamado #${data.id} atualizado`, `O status ou responsável foi alterado.`);
}
// Atualiza as listas, os stats e o detalhe do ticket
queryClient.invalidateQueries({ queryKey: ['tickets'] });
queryClient.invalidateQueries({ queryKey: ['stats'] });
queryClient.invalidateQueries({ queryKey: ['ticket', data.id] });
});
// Quando um comentário é adicionado
eventSource.addEventListener('comment.added', (e) => {
const data = JSON.parse(e.data);
if (user && (user.id === data.creatorId || user.id === data.assigneeId) && user.id !== data.authorId) {
if (!data.isInternal || (data.isInternal && user.role !== 'requester')) {
notify(`Nova mensagem no chamado #${data.ticketId}`, 'Você tem uma nova interação no chamado.');
}
}
// Atualiza os comentários do ticket afetado
queryClient.invalidateQueries({ queryKey: ['tickets', data.ticketId, 'comments'] });
});
// Tratamento de erro na conexão
eventSource.onerror = (err) => {
console.error('Erro na conexão SSE:', err);
// O navegador tentará reconectar automaticamente
};
return () => {
eventSource.close();
};
}, [token, isAuthenticated, queryClient, addToast]);
}
|