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:
parent
2f766af902
commit
695a44781a
4 changed files with 55 additions and 37 deletions
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue