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

View file

@ -274,22 +274,19 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
const [openCalendarField, setOpenCalendarField] = useState<string | null>(null) const [openCalendarField, setOpenCalendarField] = useState<string | null>(null)
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const [validationError, setValidationError] = useState<string | null>(null) 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( const initialValues = useMemo(
() => buildInitialValues(selectedForm.fields, currentFields), () => buildInitialValues(selectedForm.fields, ticket.customFields),
[selectedForm.fields, currentFields] [selectedForm.fields, ticket.customFields]
) )
useEffect(() => { // Funcao para abrir o editor e resetar os valores
if (!editorOpen) return const openEditor = () => {
setCustomFieldValues(initialValues) setCustomFieldValues(initialValues)
setValidationError(null) setValidationError(null)
}, [editorOpen, initialValues]) setEditorOpen(true)
}
const updateCustomFields = useMutation(api.tickets.updateCustomFields) const updateCustomFields = useMutation(api.tickets.updateCustomFields)
@ -333,12 +330,11 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
setIsSaving(true) setIsSaving(true)
setValidationError(null) setValidationError(null)
try { try {
const result = await updateCustomFields({ await updateCustomFields({
ticketId: ticket.id as Id<"tickets">, ticketId: ticket.id as Id<"tickets">,
actorId: viewerId, actorId: viewerId,
fields: payload, fields: payload,
}) })
setCurrentFields(result?.customFields ?? currentFields)
toast.success("Campos personalizados atualizados!", { id: "ticket-custom-fields" }) toast.success("Campos personalizados atualizados!", { id: "ticket-custom-fields" })
setEditorOpen(false) setEditorOpen(false)
} catch (error) { } catch (error) {
@ -355,7 +351,7 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
canEdit && hasConfiguredFields ? ( canEdit && hasConfiguredFields ? (
<button <button
type="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" 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"> <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 ( return (
<> <>
<TicketCustomFieldsList <TicketCustomFieldsList
record={currentFields} record={ticket.customFields}
emptyMessage="Nenhum campo adicional preenchido neste chamado." emptyMessage="Nenhum campo adicional preenchido neste chamado."
actionSlot={editActionSlot} actionSlot={editActionSlot}
className={listClassName} 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> <h3 className="text-sm font-semibold text-neutral-900">Informações adicionais</h3>
</div> </div>
<TicketCustomFieldsList <TicketCustomFieldsList
record={currentFields} record={ticket.customFields}
emptyMessage="Nenhum campo adicional preenchido neste chamado." emptyMessage="Nenhum campo adicional preenchido neste chamado."
actionSlot={editActionSlot} actionSlot={editActionSlot}
/> />

View file

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