Melhora chat ao vivo com anexos e eventos de timeline

- Reestrutura visual do widget de chat (header branco, status emerald)
- Adiciona sistema de anexos com upload e drag-and-drop
- Substitui select nativo por componente Select do shadcn
- Adiciona eventos LIVE_CHAT_STARTED e LIVE_CHAT_ENDED na timeline
- Traduz labels de chat para portugues (Chat iniciado/finalizado)
- Filtra CHAT_MESSAGE_ADDED da timeline (apenas inicio/fim aparecem)
- Restringe inicio de chat a tickets com responsavel atribuido
- Exibe duracao da sessao ao finalizar chat

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 02:20:11 -03:00
parent 9e676b06f9
commit 3b1cde79df
11 changed files with 782 additions and 77 deletions

View file

@ -25,20 +25,20 @@ async function validateMachineToken(
.first()
if (!tokenRecord) {
throw new ConvexError("Token de maquina invalido")
throw new ConvexError("Token de máquina inválido")
}
if (tokenRecord.revoked) {
throw new ConvexError("Token de maquina revogado")
throw new ConvexError("Token de máquina revogado")
}
if (tokenRecord.expiresAt < Date.now()) {
throw new ConvexError("Token de maquina expirado")
throw new ConvexError("Token de máquina expirado")
}
const machine = await ctx.db.get(tokenRecord.machineId)
if (!machine) {
throw new ConvexError("Maquina nao encontrada")
throw new ConvexError("Máquina não encontrada")
}
return { machine, tenantId: tokenRecord.tenantId }
@ -57,11 +57,11 @@ export const startSession = mutation({
handler: async (ctx, { ticketId, actorId }) => {
const ticket = await ctx.db.get(ticketId)
if (!ticket) {
throw new ConvexError("Ticket nao encontrado")
throw new ConvexError("Ticket não encontrado")
}
if (!ticket.machineId) {
throw new ConvexError("Este ticket nao esta vinculado a uma maquina")
throw new ConvexError("Este ticket não está vinculado a uma máquina")
}
// Verificar se agente tem permissao
@ -78,12 +78,12 @@ export const startSession = mutation({
// Verificar se maquina esta online (heartbeat nos ultimos 5 minutos)
const machine = await ctx.db.get(ticket.machineId)
if (!machine) {
throw new ConvexError("Maquina nao encontrada")
throw new ConvexError("Máquina não encontrada")
}
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
if (!machine.lastHeartbeatAt || machine.lastHeartbeatAt < fiveMinutesAgo) {
throw new ConvexError("Maquina offline. A maquina precisa estar online para iniciar o chat.")
throw new ConvexError("Máquina offline. A máquina precisa estar online para iniciar o chat.")
}
// Verificar se ja existe sessao ativa para este ticket
@ -123,6 +123,19 @@ export const startSession = mutation({
await ctx.db.patch(ticketId, { chatEnabled: true })
}
// Registrar evento na timeline
await ctx.db.insert("ticketEvents", {
ticketId,
type: "LIVE_CHAT_STARTED",
payload: {
sessionId,
agentId: actorId,
agentName: agent.name,
machineHostname: machine.hostname,
},
createdAt: now,
})
return { sessionId, isNew: true }
},
})
@ -136,7 +149,7 @@ export const endSession = mutation({
handler: async (ctx, { sessionId, actorId }) => {
const session = await ctx.db.get(sessionId)
if (!session) {
throw new ConvexError("Sessao nao encontrada")
throw new ConvexError("Sessão não encontrada")
}
// Verificar permissao
@ -146,12 +159,32 @@ export const endSession = mutation({
}
if (session.status !== "ACTIVE") {
throw new ConvexError("Sessao ja encerrada")
throw new ConvexError("Sessão já encerrada")
}
const now = Date.now()
await ctx.db.patch(sessionId, {
status: "ENDED",
endedAt: Date.now(),
endedAt: now,
})
// Calcular duracao da sessao
const durationMs = now - session.startedAt
// Registrar evento na timeline
await ctx.db.insert("ticketEvents", {
ticketId: session.ticketId,
type: "LIVE_CHAT_ENDED",
payload: {
sessionId,
agentId: actorId,
agentName: agent.name,
durationMs,
startedAt: session.startedAt,
endedAt: now,
},
createdAt: now,
})
return { ok: true }
@ -184,11 +217,11 @@ export const postMachineMessage = mutation({
const ticket = await ctx.db.get(args.ticketId)
if (!ticket || ticket.tenantId !== tenantId) {
throw new ConvexError("Ticket nao encontrado")
throw new ConvexError("Ticket não encontrado")
}
if (ticket.machineId?.toString() !== machine._id.toString()) {
throw new ConvexError("Esta maquina nao esta vinculada a este ticket")
throw new ConvexError("Esta máquina não está vinculada a este ticket")
}
// Verificar se existe sessao ativa
@ -199,7 +232,7 @@ export const postMachineMessage = mutation({
.first()
if (!session) {
throw new ConvexError("Nenhuma sessao de chat ativa para este ticket")
throw new ConvexError("Nenhuma sessão de chat ativa para este ticket")
}
// Obter usuario vinculado a maquina (ou usar nome do hostname)
@ -233,7 +266,7 @@ export const postMachineMessage = mutation({
// Limitar tamanho do body
if (args.body.length > 4000) {
throw new ConvexError("Mensagem muito longa (maximo 4000 caracteres)")
throw new ConvexError("Mensagem muito longa (máximo 4000 caracteres)")
}
// Inserir mensagem
@ -272,11 +305,11 @@ export const markMachineMessagesRead = mutation({
const ticket = await ctx.db.get(args.ticketId)
if (!ticket || ticket.tenantId !== tenantId) {
throw new ConvexError("Ticket nao encontrado")
throw new ConvexError("Ticket não encontrado")
}
if (ticket.machineId?.toString() !== machine._id.toString()) {
throw new ConvexError("Esta maquina nao esta vinculada a este ticket")
throw new ConvexError("Esta máquina não está vinculada a este ticket")
}
// Obter userId para marcar leitura
@ -367,11 +400,11 @@ export const listMachineMessages = query({
const ticket = await ctx.db.get(args.ticketId)
if (!ticket || ticket.tenantId !== tenantId) {
throw new ConvexError("Ticket nao encontrado")
throw new ConvexError("Ticket não encontrado")
}
if (ticket.machineId?.toString() !== machine._id.toString()) {
throw new ConvexError("Esta maquina nao esta vinculada a este ticket")
throw new ConvexError("Esta máquina não está vinculada a este ticket")
}
// Buscar sessao ativa