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:
parent
3bfc5793f1
commit
98b23af4b2
3 changed files with 105 additions and 15 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -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" })
|
||||||
return
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss("work")
|
toast.dismiss("work")
|
||||||
toast.loading("Iniciando atendimento...", { id: "work" })
|
toast.loading("Iniciando atendimento...", { id: "work" })
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue