refactor(tickets): corrige avatar desincronizado e otimiza performance
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 7s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 4m26s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
Quality Checks / Lint, Test and Build (push) Successful in 5m1s

- Corrige avatar desincronizado na listagem de tickets usando sessao do usuario logado
- Otimiza timer da tickets-table de 1s para 15s (reduz re-renders)
- Remove useEffect desnecessario em status-select (usa prop diretamente)
- Remove useEffects desnecessarios em ticket-custom-fields (usa ticket.customFields diretamente)
- Adiciona React.memo no AssigneeCell para evitar re-renders

🤖 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-17 09:35:51 -03:00
parent f617916fe7
commit aa9c09c30e
3 changed files with 27 additions and 30 deletions

View file

@ -1,6 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import { useState } from "react"
import type { Id } from "@/convex/_generated/dataModel"
import type { TicketStatus } from "@/lib/schemas/ticket"
import { useAuth } from "@/lib/auth-client"
@ -54,18 +54,13 @@ export function StatusSelect({
: machineAssignedName && machineAssignedName.length > 0
? machineAssignedName
: null
const [status, setStatus] = useState<TicketStatus>(value)
const [closeDialogOpen, setCloseDialogOpen] = useState(false)
useEffect(() => {
setStatus(value)
}, [value])
return (
<>
<div className="inline-flex items-center gap-2">
<Badge className={cn(baseBadgeClass, statusStyles[status]?.badgeClass ?? statusStyles.PENDING.badgeClass)}>
{statusStyles[status]?.label ?? statusStyles.PENDING.label}
<Badge className={cn(baseBadgeClass, statusStyles[value]?.badgeClass ?? statusStyles.PENDING.badgeClass)}>
{statusStyles[value]?.label ?? statusStyles.PENDING.label}
</Badge>
{showCloseButton ? (
<Tooltip>
@ -100,7 +95,6 @@ export function StatusSelect({
requesterName={requesterName}
agentName={agentName}
onSuccess={() => {
setStatus("RESOLVED")
setCloseDialogOpen(false)
onStatusChange?.("RESOLVED")
}}

View file

@ -274,22 +274,19 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
const [openCalendarField, setOpenCalendarField] = useState<string | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [validationError, setValidationError] = useState<string | null>(null)
const [currentFields, setCurrentFields] = useState<TicketCustomFieldRecord | null | undefined>(ticket.customFields)
useEffect(() => {
setCurrentFields(ticket.customFields)
}, [ticket.customFields])
// Usa ticket.customFields diretamente (Convex atualiza automaticamente)
const initialValues = useMemo(
() => buildInitialValues(selectedForm.fields, currentFields),
[selectedForm.fields, currentFields]
() => buildInitialValues(selectedForm.fields, ticket.customFields),
[selectedForm.fields, ticket.customFields]
)
useEffect(() => {
if (!editorOpen) return
// Funcao para abrir o editor e resetar os valores
const openEditor = () => {
setCustomFieldValues(initialValues)
setValidationError(null)
}, [editorOpen, initialValues])
setEditorOpen(true)
}
const updateCustomFields = useMutation(api.tickets.updateCustomFields)
@ -333,12 +330,11 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
setIsSaving(true)
setValidationError(null)
try {
const result = await updateCustomFields({
await updateCustomFields({
ticketId: ticket.id as Id<"tickets">,
actorId: viewerId,
fields: payload,
})
setCurrentFields(result?.customFields ?? currentFields)
toast.success("Campos personalizados atualizados!", { id: "ticket-custom-fields" })
setEditorOpen(false)
} catch (error) {
@ -355,7 +351,7 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
canEdit && hasConfiguredFields ? (
<button
type="button"
onClick={() => setEditorOpen(true)}
onClick={openEditor}
className="flex h-full min-h-[88px] w-full flex-col items-start justify-center gap-1 rounded-2xl border-2 border-dashed border-slate-300 bg-slate-50/60 px-4 py-3 text-left text-sm font-semibold text-neutral-700 transition hover:border-slate-400 hover:bg-white"
>
<span className="inline-flex items-center gap-2 text-sm font-semibold text-neutral-900">
@ -402,7 +398,7 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
return (
<>
<TicketCustomFieldsList
record={currentFields}
record={ticket.customFields}
emptyMessage="Nenhum campo adicional preenchido neste chamado."
actionSlot={editActionSlot}
className={listClassName}
@ -418,7 +414,7 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
<h3 className="text-sm font-semibold text-neutral-900">Informações adicionais</h3>
</div>
<TicketCustomFieldsList
record={currentFields}
record={ticket.customFields}
emptyMessage="Nenhum campo adicional preenchido neste chamado."
actionSlot={editActionSlot}
/>

View file

@ -1,12 +1,13 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { memo, useEffect, useRef, useState } from "react"
import { useRouter } from "next/navigation"
import { format, formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale"
import { IconBuildingOff, IconCategory, IconHierarchyOff, IconUserOff } from "@tabler/icons-react"
import type { Ticket } from "@/lib/schemas/ticket"
import { useAuth } from "@/lib/auth-client"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
@ -60,7 +61,7 @@ function formatQueueLabel(queue?: string | null) {
return { label: queue, title: queue }
}
function AssigneeCell({ ticket }: { ticket: Ticket }) {
const AssigneeCell = memo(function AssigneeCell({ ticket, sessionUserId, sessionAvatarUrl }: { ticket: Ticket; sessionUserId?: string; sessionAvatarUrl?: string | null }) {
if (!ticket.assignee) {
return (
<div className="flex items-center justify-center">
@ -79,10 +80,14 @@ function AssigneeCell({ ticket }: { ticket: Ticket }) {
.map((part) => part[0]?.toUpperCase())
.join("")
// Usa o avatar da sessao se o assignee for o usuario logado (garante sincronizacao)
const isCurrentUser = sessionUserId && ticket.assignee.id === sessionUserId
const avatarUrl = isCurrentUser ? sessionAvatarUrl : ticket.assignee.avatarUrl
return (
<div className="flex min-w-0 flex-col items-center gap-2 text-center">
<Avatar className="size-8 border border-slate-200">
<AvatarImage src={ticket.assignee.avatarUrl} alt={ticket.assignee.name} />
<AvatarImage src={avatarUrl ?? undefined} alt={ticket.assignee.name} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col items-center">
@ -95,7 +100,7 @@ function AssigneeCell({ ticket }: { ticket: Ticket }) {
</div>
</div>
)
}
})
export type TicketsTableProps = {
tickets?: Ticket[]
@ -107,11 +112,13 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) {
const [now, setNow] = useState(() => Date.now())
const serverOffsetRef = useRef<number>(0)
const router = useRouter()
const { session } = useAuth()
// Atualiza o timer a cada 15 segundos (otimizado de 1s)
useEffect(() => {
const interval = setInterval(() => {
setNow(Date.now())
}, 1000)
}, 15000)
return () => clearInterval(interval)
}, [])
@ -325,7 +332,7 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) {
</div>
</TableCell>
<TableCell className={`${borderedCellClass} hidden xl:table-cell overflow-hidden`}>
<AssigneeCell ticket={ticket} />
<AssigneeCell ticket={ticket} sessionUserId={session?.user?.id} sessionAvatarUrl={session?.user?.avatarUrl} />
</TableCell>
<TableCell className={borderedCellClass}>
<div className="flex flex-col items-center leading-tight text-center">