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"
|
"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")
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue