From 14480df9f3647e42cec8bae633a4a1f8a6b7bb59 Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Wed, 17 Dec 2025 18:57:58 -0300 Subject: [PATCH] feat(chat): vincula timer automaticamente com inicio/fim do chat ao vivo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ao iniciar chat: inicia timer EXTERNAL automaticamente se nao houver sessao ativa - Ao encerrar chat: pausa timer automaticamente se houver sessao ativa - Adiciona razao de pausa END_LIVE_CHAT para identificar pausas automaticas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- convex/liveChat.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++-- convex/tickets.ts | 1 + 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/convex/liveChat.ts b/convex/liveChat.ts index 3a5ceec..0c700ae 100644 --- a/convex/liveChat.ts +++ b/convex/liveChat.ts @@ -168,7 +168,40 @@ export const startSession = mutation({ createdAt: now, }) - return { sessionId, isNew: true } + // Iniciar timer automaticamente se nao houver sessao de trabalho ativa + // O chat ao vivo eh considerado trabalho EXTERNAL (interacao com cliente) + let workSessionId: Id<"ticketWorkSessions"> | null = null + if (!ticket.activeSessionId && ticket.assigneeId) { + workSessionId = await ctx.db.insert("ticketWorkSessions", { + ticketId, + agentId: ticket.assigneeId, + workType: "EXTERNAL", + startedAt: now, + }) + + await ctx.db.patch(ticketId, { + working: true, + activeSessionId: workSessionId, + status: "AWAITING_ATTENDANCE", + updatedAt: now, + }) + + await ctx.db.insert("ticketEvents", { + ticketId, + type: "WORK_STARTED", + payload: { + actorId, + actorName: agent.name, + actorAvatar: agent.avatarUrl, + sessionId: workSessionId, + workType: "EXTERNAL", + source: "live_chat_auto", + }, + createdAt: now, + }) + } + + return { sessionId, isNew: true, workSessionStarted: workSessionId !== null } }, }) @@ -225,7 +258,60 @@ export const endSession = mutation({ createdAt: now, }) - return { ok: true } + // Pausar timer automaticamente se houver sessao de trabalho ativa + let workSessionPaused = false + const ticket = await ctx.db.get(session.ticketId) + if (ticket?.activeSessionId) { + const workSession = await ctx.db.get(ticket.activeSessionId) + if (workSession && !workSession.stoppedAt) { + const workDurationMs = now - workSession.startedAt + const sessionType = (workSession.workType ?? "INTERNAL").toUpperCase() + const deltaInternal = sessionType === "INTERNAL" ? workDurationMs : 0 + const deltaExternal = sessionType === "EXTERNAL" ? workDurationMs : 0 + + // Encerrar sessao de trabalho + await ctx.db.patch(ticket.activeSessionId, { + stoppedAt: now, + durationMs: workDurationMs, + pauseReason: "END_LIVE_CHAT", + pauseNote: "Pausa automática ao encerrar chat ao vivo", + }) + + // Atualizar ticket + await ctx.db.patch(session.ticketId, { + working: false, + activeSessionId: undefined, + status: "PAUSED", + totalWorkedMs: (ticket.totalWorkedMs ?? 0) + workDurationMs, + internalWorkedMs: (ticket.internalWorkedMs ?? 0) + deltaInternal, + externalWorkedMs: (ticket.externalWorkedMs ?? 0) + deltaExternal, + updatedAt: now, + }) + + // Registrar evento de pausa + await ctx.db.insert("ticketEvents", { + ticketId: session.ticketId, + type: "WORK_PAUSED", + payload: { + actorId, + actorName: actor.name, + actorAvatar: actor.avatarUrl, + sessionId: workSession._id, + sessionDurationMs: workDurationMs, + workType: sessionType, + pauseReason: "END_LIVE_CHAT", + pauseReasonLabel: "Chat ao vivo encerrado", + pauseNote: "Pausa automática ao encerrar chat ao vivo", + source: "live_chat_auto", + }, + createdAt: now, + }) + + workSessionPaused = true + } + } + + return { ok: true, workSessionPaused } }, }) diff --git a/convex/tickets.ts b/convex/tickets.ts index 8ab5ebe..27922cc 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -38,6 +38,7 @@ const PAUSE_REASON_LABELS: Record = { NO_CONTACT: "Falta de contato", WAITING_THIRD_PARTY: "Aguardando terceiro", IN_PROCEDURE: "Em procedimento", + END_LIVE_CHAT: "Chat ao vivo encerrado", [LUNCH_BREAK_REASON]: LUNCH_BREAK_PAUSE_LABEL, }; const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;