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:
parent
9e676b06f9
commit
3b1cde79df
11 changed files with 782 additions and 77 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue