Corrige badge de mensagens nao lidas no chat web e desktop

- Web: markChatRead agora zera unreadByAgent na sessao ativa
- Desktop: usa unreadCount do backend ao inves de calcular localmente
- Backend: listMachineMessages retorna unreadCount da sessao
- Centraliza colunas da tabela de tickets do dispositivo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-10 22:48:18 -03:00
parent 2f766af902
commit 695a44781a
4 changed files with 55 additions and 37 deletions

View file

@ -94,8 +94,9 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
setIsLoading(false)
setHasSession(payload.hasSession)
hadSessionRef.current = hadSessionRef.current || payload.hasSession
const unreadMessages = payload.messages.filter(m => !m.isFromMachine)
setUnreadCount(unreadMessages.length)
// Usa o unreadCount do backend (baseado em unreadByMachine da sessao)
const backendUnreadCount = (payload as { unreadCount?: number }).unreadCount ?? 0
setUnreadCount(backendUnreadCount)
setMessages(prev => {
const existingIds = new Set(prev.map(m => m.id))
const combined = [...prev, ...payload.messages.filter(m => !existingIds.has(m.id))]
@ -106,11 +107,12 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
const first = payload.messages[0]
setTicketInfo((prevInfo) => prevInfo ?? { ref: 0, subject: "", agentName: first.authorName ?? "Suporte" })
}
// Só marca como lidas se a janela estiver expandida (evita perder badge ao minimizar)
const unreadIds = unreadMessages.map(m => m.id as string)
if (unreadIds.length > 0 && !isMinimized) {
// Marca como lidas se a janela estiver expandida e houver nao lidas
if (backendUnreadCount > 0 && !isMinimized) {
const unreadIds = payload.messages.filter(m => !m.isFromMachine).map(m => m.id as string)
if (unreadIds.length > 0) {
markMachineMessagesRead(ticketId, unreadIds).catch(err => console.error("mark read falhou", err))
setUnreadCount(0)
}
}
},
(err) => {
@ -139,15 +141,16 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
return () => window.removeEventListener("resize", handler)
}, [])
// Quando expandir, marcar mensagens como lidas e limpar badge
// Quando expandir, marcar mensagens como lidas (o backend zera unreadByMachine e a reatividade atualiza)
useEffect(() => {
if (isMinimized) return
if (unreadCount === 0) return
const unreadIds = messages.filter(m => !m.isFromMachine).map(m => m.id as string)
if (unreadIds.length > 0) {
markMachineMessagesRead(ticketId, unreadIds).catch(err => console.error("mark read falhou", err))
}
setUnreadCount(0)
}, [isMinimized, messages, ticketId])
// Nao setamos unreadCount aqui - o backend vai zerar unreadByMachine e a subscription vai atualizar
}, [isMinimized, messages, ticketId, unreadCount])
// Selecionar arquivo para anexar
const handleAttach = async () => {
@ -249,12 +252,8 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
} catch (err) {
console.error("Erro ao expandir janela:", err)
}
// Marca mensagens como lidas ao expandir
const unreadIds = messages.filter(m => !m.isFromMachine).map(m => m.id as string)
if (unreadIds.length > 0) {
markMachineMessagesRead(ticketId, unreadIds).catch(err => console.error("mark read falhou", err))
}
setUnreadCount(0)
// Marca mensagens como lidas ao expandir (o useEffect ja cuida disso quando isMinimized muda)
// O backend zera unreadByMachine e a subscription atualiza automaticamente
}
const handleClose = () => {

View file

@ -467,7 +467,7 @@ export const listMachineMessages = query({
.first()
if (!session) {
return { messages: [], hasSession: false }
return { messages: [], hasSession: false, unreadCount: 0 }
}
// Aplicar limite (máximo 100 mensagens por chamada)
@ -505,7 +505,7 @@ export const listMachineMessages = query({
}
})
return { messages: result, hasSession: true }
return { messages: result, hasSession: true, unreadCount: session.unreadByMachine ?? 0 }
},
})

View file

@ -3438,6 +3438,18 @@ export const markChatRead = mutation({
updatedAt: now,
})
}
// Zerar contador de nao lidas pelo agente na sessao ativa
const session = await ctx.db
.query("liveChatSessions")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
.filter((q) => q.eq(q.field("status"), "ACTIVE"))
.first()
if (session) {
await ctx.db.patch(session._id, { unreadByAgent: 0 })
}
return { ok: true }
},
})

View file

@ -353,23 +353,30 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: {
) : (
<>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-b border-slate-200 bg-slate-50/60">
<TableHead className="min-w-[260px] text-xs font-semibold uppercase tracking-wide text-neutral-500">
<Table className="w-full" style={{ tableLayout: "fixed", minWidth: "900px" }}>
<colgroup>
<col style={{ width: "300px" }} />
<col style={{ width: "140px" }} />
<col style={{ width: "140px" }} />
<col style={{ width: "180px" }} />
<col style={{ width: "200px" }} />
</colgroup>
<TableHeader className="bg-slate-100/80">
<TableRow className="border-b border-slate-200 bg-transparent text-[11px] uppercase tracking-wide text-neutral-600 hover:bg-transparent">
<TableHead className="px-4 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
Ticket
</TableHead>
<TableHead className="w-[140px] text-xs font-semibold uppercase tracking-wide text-neutral-500">
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
Status
</TableHead>
<TableHead className="w-[140px] text-xs font-semibold uppercase tracking-wide text-neutral-500">
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
Prioridade
</TableHead>
<TableHead className="w-[160px] text-xs font-semibold uppercase tracking-wide text-neutral-500">
Última atualização
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
Ultima atualizacao
</TableHead>
<TableHead className="w-[200px] text-xs font-semibold uppercase tracking-wide text-neutral-500">
Responsável
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
Responsavel
</TableHead>
</TableRow>
</TableHeader>
@ -381,15 +388,15 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: {
const updatedAbsolute = formatAbsoluteTime(ticket.updatedAt)
return (
<TableRow key={ticket.id} className="border-b border-slate-100 hover:bg-slate-50/70">
<TableCell className="align-top">
<div className="flex flex-col gap-1">
<TableCell className="px-4 py-3 text-center align-middle">
<div className="flex flex-col items-center gap-1">
<Link
href={`/tickets/${ticket.id}`}
className="text-sm font-semibold text-neutral-900 underline-offset-4 hover:underline"
>
#{ticket.reference} · {ticket.subject}
</Link>
<div className="flex flex-wrap items-center gap-2 text-xs text-neutral-500">
<div className="flex flex-wrap items-center justify-center gap-2 text-xs text-neutral-500">
<span>{requesterLabel}</span>
{ticket.queue ? (
<span className="inline-flex items-center rounded-full bg-slate-200/70 px-2 py-0.5 text-[11px] font-medium text-neutral-600">
@ -400,22 +407,22 @@ export function DeviceTicketsHistoryClient({ tenantId: _tenantId, deviceId }: {
</div>
</div>
</TableCell>
<TableCell className="align-top">
<TableCell className="px-3 py-3 text-center align-middle">
<TicketStatusBadge status={ticket.status} className="h-7 px-3 text-xs font-semibold" />
</TableCell>
<TableCell className="align-top">
<TableCell className="px-3 py-3 text-center align-middle">
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold", priorityMeta.badgeClass)}>
{priorityMeta.label}
</Badge>
</TableCell>
<TableCell className="align-top">
<div className="flex flex-col text-xs text-neutral-600">
<TableCell className="px-3 py-3 text-center align-middle">
<div className="flex flex-col items-center text-xs text-neutral-600">
<span className="font-medium text-neutral-800">{updatedLabel}</span>
<span className="text-[11px] text-neutral-400">{updatedAbsolute}</span>
</div>
</TableCell>
<TableCell className="align-top">
<div className="flex flex-col items-start text-sm text-neutral-700">
<TableCell className="px-3 py-3 text-center align-middle">
<div className="flex flex-col items-center text-sm text-neutral-700">
{ticket.assignee ? (
<>
<span>{ticket.assignee.name ?? ticket.assignee.email ?? "—"}</span>