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

View file

@ -467,7 +467,7 @@ export const listMachineMessages = query({
.first() .first()
if (!session) { if (!session) {
return { messages: [], hasSession: false } return { messages: [], hasSession: false, unreadCount: 0 }
} }
// Aplicar limite (máximo 100 mensagens por chamada) // 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, 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 } return { ok: true }
}, },
}) })

View file

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