refactor(tickets): corrige avatar desincronizado e otimiza performance
All checks were successful
All checks were successful
- 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:
parent
f617916fe7
commit
aa9c09c30e
3 changed files with 27 additions and 30 deletions
|
|
@ -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")
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue