feat(tickets): Play e Chat atribuem responsavel automaticamente

- Botao Play habilitado mesmo sem responsavel
- Ao clicar Play sem responsavel, atribui usuario logado automaticamente
- Ao iniciar chat ao vivo sem responsavel, atribui usuario logado
- Adiciona mutation fixLegacySessions para corrigir sessoes antigas

🤖 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-15 16:10:13 -03:00
parent 3bfc5793f1
commit 98b23af4b2
3 changed files with 105 additions and 15 deletions

View file

@ -118,11 +118,13 @@ export function ChatHubWidget() {
} }
const handleExpand = async () => { const handleExpand = async () => {
setIsMinimized(false)
try { try {
await invoke("set_hub_minimized", { minimized: false }) await invoke("set_hub_minimized", { minimized: false })
// Aguarda a janela redimensionar antes de atualizar o estado
setTimeout(() => setIsMinimized(false), 100)
} catch (err) { } catch (err) {
console.error("Erro ao expandir hub:", err) console.error("Erro ao expandir hub:", err)
setIsMinimized(false) // Fallback
} }
} }
@ -201,7 +203,7 @@ export function ChatHubWidget() {
// Expandido - mostrar lista // Expandido - mostrar lista
return ( return (
<div className="flex h-screen flex-col overflow-hidden rounded-2xl bg-white shadow-xl"> <div className="flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-xl">
<ChatSessionList <ChatSessionList
sessions={sessions} sessions={sessions}
onSelectSession={handleSelectSession} onSelectSession={handleSelectSession}

View file

@ -896,6 +896,47 @@ export const autoEndInactiveSessions = mutation({
}, },
}) })
// Mutation para corrigir sessoes antigas sem campos obrigatorios
export const fixLegacySessions = mutation({
args: {},
handler: async (ctx) => {
const now = Date.now()
// Buscar sessoes ativas que podem ter campos faltando
const activeSessions = await ctx.db
.query("liveChatSessions")
.withIndex("by_status_lastActivity", (q) => q.eq("status", "ACTIVE"))
.take(100)
let fixed = 0
let ended = 0
for (const session of activeSessions) {
// Se sessao nao tem lastAgentMessageAt, adiciona o valor de startedAt
if (session.lastAgentMessageAt === undefined) {
// Sessao muito antiga (mais de 24h) - encerrar
if (now - session.startedAt > 24 * 60 * 60 * 1000) {
await ctx.db.patch(session._id, {
status: "ENDED",
endedAt: now,
lastAgentMessageAt: session.lastActivityAt ?? session.startedAt,
})
ended++
} else {
// Sessao recente - apenas corrigir o campo
await ctx.db.patch(session._id, {
lastAgentMessageAt: session.lastActivityAt ?? session.startedAt,
})
fixed++
}
}
}
console.log(`fixLegacySessions: fixed=${fixed}, ended=${ended}`)
return { fixed, ended, total: activeSessions.length }
},
})
// ============================================ // ============================================
// UPLOAD DE ARQUIVOS (Maquina/Cliente) // UPLOAD DE ARQUIVOS (Maquina/Cliente)
// ============================================ // ============================================

View file

@ -343,13 +343,38 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const machineOnline = liveChatSession?.machineOnline ?? false const machineOnline = liveChatSession?.machineOnline ?? false
const hasActiveSession = Boolean(liveChatSession?.sessionId) const hasActiveSession = Boolean(liveChatSession?.sessionId)
const ticketHasAssignee = Boolean(ticket.assignee) const ticketHasAssignee = Boolean(ticket.assignee)
const canStartChat = hasMachine && !hasActiveSession && isStaff && ticketHasAssignee // Permitir iniciar chat mesmo sem responsavel - vai atribuir automaticamente
const canStartChat = hasMachine && !hasActiveSession && isStaff
const handleStartLiveChat = async () => { const handleStartLiveChat = async () => {
if (!convexUserId || !ticket.id || isStartingChat) return if (!convexUserId || !ticket.id || isStartingChat) return
setIsStartingChat(true) setIsStartingChat(true)
toast.dismiss("live-chat") toast.dismiss("live-chat")
try { try {
// Se nao ha responsavel, atribuir o usuario logado automaticamente
if (!ticketHasAssignee) {
toast.loading("Atribuindo responsavel...", { id: "live-chat" })
await changeAssignee({
ticketId: ticket.id as Id<"tickets">,
assigneeId: convexUserId as Id<"users">,
actorId: convexUserId as Id<"users">,
reason: "Atribuido automaticamente ao iniciar chat ao vivo",
})
// Atualizar estado local
const agent = agents.find((a) => String(a._id) === convexUserId)
if (agent) {
setAssigneeState({
id: String(agent._id),
name: agent.name,
email: agent.email,
avatarUrl: agent.avatarUrl ?? undefined,
teams: Array.isArray(agent.teams) ? agent.teams.filter((t): t is string => typeof t === "string") : [],
})
}
}
toast.loading("Iniciando chat ao vivo...", { id: "live-chat" })
const result = await startLiveChat({ const result = await startLiveChat({
ticketId: ticket.id as Id<"tickets">, ticketId: ticket.id as Id<"tickets">,
actorId: convexUserId as Id<"users">, actorId: convexUserId as Id<"users">,
@ -357,18 +382,18 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
if (result.isNew) { if (result.isNew) {
toast.success("Chat ao vivo iniciado", { id: "live-chat" }) toast.success("Chat ao vivo iniciado", { id: "live-chat" })
} else { } else {
toast.info("Já existe uma sessão de chat ativa", { id: "live-chat" }) toast.info("Ja existe uma sessao de chat ativa", { id: "live-chat" })
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error("[LiveChat] Erro ao iniciar chat:", error) console.error("[LiveChat] Erro ao iniciar chat:", error)
// Extrair mensagem amigável do erro do Convex // Extrair mensagem amigável do erro do Convex
let message = "Não foi possível iniciar o chat" let message = "Nao foi possivel iniciar o chat"
if (error instanceof Error) { if (error instanceof Error) {
const errorMsg = error.message.toLowerCase() const errorMsg = error.message.toLowerCase()
if (errorMsg.includes("offline")) { if (errorMsg.includes("offline")) {
message = "Máquina offline. Aguarde a máquina ficar online para iniciar o chat." message = "Maquina offline. Aguarde a maquina ficar online para iniciar o chat."
} else if (errorMsg.includes("não encontrad") || errorMsg.includes("not found")) { } else if (errorMsg.includes("nao encontrad") || errorMsg.includes("not found")) {
message = "Máquina não encontrada" message = "Maquina nao encontrada"
} }
} }
toast.error(message, { id: "live-chat" }) toast.error(message, { id: "live-chat" })
@ -597,7 +622,8 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const hasAssignee = Boolean(currentAssigneeId) const hasAssignee = Boolean(currentAssigneeId)
const isCurrentResponsible = hasAssignee && convexUserId ? currentAssigneeId === convexUserId : false const isCurrentResponsible = hasAssignee && convexUserId ? currentAssigneeId === convexUserId : false
const isResolved = status === "RESOLVED" const isResolved = status === "RESOLVED"
const canControlWork = !isResolved && isStaff && hasAssignee // Permitir Play mesmo sem responsavel - vai atribuir automaticamente
const canControlWork = !isResolved && isStaff
const canPauseWork = !isResolved && (isAdmin || isCurrentResponsible) const canPauseWork = !isResolved && (isAdmin || isCurrentResponsible)
const pauseDisabled = !canPauseWork const pauseDisabled = !canPauseWork
const startDisabled = !canControlWork const startDisabled = !canControlWork
@ -605,14 +631,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
if (isResolved) { if (isResolved) {
return "Este chamado está encerrado. Reabra o ticket para iniciar um novo atendimento." return "Este chamado está encerrado. Reabra o ticket para iniciar um novo atendimento."
} }
if (!hasAssignee) {
return "Defina um responsável antes de iniciar o atendimento."
}
if (!isStaff) { if (!isStaff) {
return "Apenas a equipe interna pode iniciar este atendimento." return "Apenas a equipe interna pode iniciar este atendimento."
} }
return "Não é possível iniciar o atendimento neste momento." return "Não é possível iniciar o atendimento neste momento."
}, [isResolved, hasAssignee, isStaff]) }, [isResolved, isStaff])
useEffect(() => { useEffect(() => {
if (!customersInitialized) { if (!customersInitialized) {
@ -1097,10 +1120,34 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const handleStartWork = async (workType: "INTERNAL" | "EXTERNAL") => { const handleStartWork = async (workType: "INTERNAL" | "EXTERNAL") => {
if (!convexUserId) return if (!convexUserId) return
// Se nao ha responsavel, atribuir o usuario logado automaticamente
if (!assigneeState?.id) { if (!assigneeState?.id) {
toast.error("Defina um responsável antes de iniciar o atendimento.") toast.loading("Atribuindo responsavel e iniciando...", { id: "work" })
try {
await changeAssignee({
ticketId: ticket.id as Id<"tickets">,
assigneeId: convexUserId as Id<"users">,
actorId: convexUserId as Id<"users">,
reason: "Atribuido automaticamente ao iniciar atendimento",
})
// Atualizar estado local
const agent = agents.find((a) => String(a._id) === convexUserId)
if (agent) {
setAssigneeState({
id: String(agent._id),
name: agent.name,
email: agent.email,
avatarUrl: agent.avatarUrl ?? undefined,
teams: Array.isArray(agent.teams) ? agent.teams.filter((t): t is string => typeof t === "string") : [],
})
}
} catch (err) {
toast.error("Nao foi possivel atribuir responsavel.", { id: "work" })
return return
} }
}
toast.dismiss("work") toast.dismiss("work")
toast.loading("Iniciando atendimento...", { id: "work" }) toast.loading("Iniciando atendimento...", { id: "work" })
try { try {