feat(chat): vincula timer automaticamente com inicio/fim do chat ao vivo

- 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 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-17 18:57:58 -03:00
parent 47ccdc51a7
commit 14480df9f3
2 changed files with 89 additions and 2 deletions

View file

@ -168,7 +168,40 @@ export const startSession = mutation({
createdAt: now, 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, 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 }
}, },
}) })

View file

@ -38,6 +38,7 @@ const PAUSE_REASON_LABELS: Record<string, string> = {
NO_CONTACT: "Falta de contato", NO_CONTACT: "Falta de contato",
WAITING_THIRD_PARTY: "Aguardando terceiro", WAITING_THIRD_PARTY: "Aguardando terceiro",
IN_PROCEDURE: "Em procedimento", IN_PROCEDURE: "Em procedimento",
END_LIVE_CHAT: "Chat ao vivo encerrado",
[LUNCH_BREAK_REASON]: LUNCH_BREAK_PAUSE_LABEL, [LUNCH_BREAK_REASON]: LUNCH_BREAK_PAUSE_LABEL,
}; };
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/; const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;