diff --git a/convex/tickets.ts b/convex/tickets.ts index 9f248a4..8b138ab 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -106,6 +106,53 @@ function normalizeTeams(teams?: string[] | null): string[] { return teams.map((team) => renameQueueString(team) ?? team); } +type RequesterFallbackContext = { + ticketId?: Id<"tickets">; + fallbackName?: string | null; + fallbackEmail?: string | null; +}; + +function buildRequesterSummary( + requester: Doc<"users"> | null, + requesterId: Id<"users">, + context?: RequesterFallbackContext, +) { + if (requester) { + return { + id: requester._id, + name: requester.name, + email: requester.email, + avatarUrl: requester.avatarUrl, + teams: normalizeTeams(requester.teams), + }; + } + + const idString = String(requesterId); + const fallbackName = + typeof context?.fallbackName === "string" && context.fallbackName.trim().length > 0 + ? context.fallbackName.trim() + : "Solicitante não encontrado"; + const fallbackEmailCandidate = + typeof context?.fallbackEmail === "string" && context.fallbackEmail.includes("@") + ? context.fallbackEmail + : null; + const fallbackEmail = fallbackEmailCandidate ?? `requester-${idString}@example.invalid`; + + if (process.env.NODE_ENV !== "test") { + const ticketInfo = context?.ticketId ? ` (ticket ${String(context.ticketId)})` : ""; + console.warn( + `[tickets] requester ${idString} ausente ao hidratar resposta${ticketInfo}; usando placeholders.`, + ); + } + + return { + id: requesterId, + name: fallbackName, + email: fallbackEmail, + teams: [], + }; +} + type CustomFieldInput = { fieldId: Id<"ticketFields">; value: unknown; @@ -348,13 +395,7 @@ export const list = query({ channel: t.channel, queue: queueName, company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null, - requester: requester && { - id: requester._id, - name: requester.name, - email: requester.email, - avatarUrl: requester.avatarUrl, - teams: normalizeTeams(requester.teams), - }, + requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }), assignee: assignee ? { id: assignee._id, @@ -526,13 +567,7 @@ export const getById = query({ channel: t.channel, queue: queueName, company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null, - requester: requester && { - id: requester._id, - name: requester.name, - email: requester.email, - avatarUrl: requester.avatarUrl, - teams: normalizeTeams(requester.teams), - }, + requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }), assignee: assignee ? { id: assignee._id, @@ -1434,13 +1469,7 @@ export const playNext = mutation({ priority: chosen.priority, channel: chosen.channel, queue: queueName, - requester: requester && { - id: requester._id, - name: requester.name, - email: requester.email, - avatarUrl: requester.avatarUrl, - teams: normalizeTeams(requester.teams), - }, + requester: buildRequesterSummary(requester, chosen.requesterId, { ticketId: chosen._id }), assignee: assignee ? { id: assignee._id, diff --git a/convex/users.ts b/convex/users.ts index bc504df..5c96db4 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -2,9 +2,6 @@ import { mutation, query } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import { requireAdmin } from "./rbac"; -// All roles that have staff-level access in some areas. Do NOT include COLLABORATOR here -// to avoid leaking collaborators into staff pickers such as "responsável". -const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]); const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]); export const ensureUser = mutation({ diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 3f75ac1..7bf3c8d 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -994,16 +994,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) { const metrics = machine?.metrics ?? null const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics]) // Live refresh the relative time label every second when we have a capture timestamp - const [relativeTick, setRelativeTick] = useState(0) + const [, setRelativeTick] = useState(0) useEffect(() => { if (!metricsCapturedAt) return const id = setInterval(() => setRelativeTick((t) => t + 1), 1000) return () => clearInterval(id) }, [metricsCapturedAt]) - const lastUpdateRelative = useMemo( - () => (metricsCapturedAt ? formatRelativeTime(metricsCapturedAt) : null), - [metricsCapturedAt, relativeTick] - ) + const lastUpdateRelative = metricsCapturedAt ? formatRelativeTime(metricsCapturedAt) : null const hardware = metadata?.hardware const network = metadata?.network ?? null const networkInterfaces = Array.isArray(network) ? network : null diff --git a/src/components/portal/portal-ticket-detail.tsx b/src/components/portal/portal-ticket-detail.tsx index 9c0a138..5bbc7c7 100644 --- a/src/components/portal/portal-ticket-detail.tsx +++ b/src/components/portal/portal-ticket-detail.tsx @@ -635,7 +635,7 @@ function PortalCommentAttachmentCard({ window.open(target, "_blank", "noopener,noreferrer") toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId }) } - }, [attachment.name, ensureUrl]) + }, [attachment.id, attachment.name, ensureUrl]) const resolvedUrl = url diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 08b0e61..1415ed1 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -650,7 +650,7 @@ function CommentAttachmentCard({ window.open(target, "_blank", "noopener,noreferrer") toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId }) } - }, [attachment.name, ensureUrl, url]) + }, [attachment.id, attachment.name, ensureUrl, url]) const name = attachment.name ?? "" const urlLooksImage = url ? /\.(png|jpe?g|gif|webp|svg)$/i.test(url) : false diff --git a/src/lib/auth-client.tsx b/src/lib/auth-client.tsx index 5cc579e..e660bfb 100644 --- a/src/lib/auth-client.tsx +++ b/src/lib/auth-client.tsx @@ -109,6 +109,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // carregar o contexto — se a API responder 200, assumimos que há sessão válida // do lado do servidor e populamos o contexto para o restante do app. useEffect(() => { + if (isPending) { + setMachineContextLoading(true) + return + } + const shouldFetch = Boolean(session?.user?.role === "machine") || !session?.user if (!shouldFetch) { setMachineContext(null) @@ -215,7 +220,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return () => { cancelled = true } - }, [session?.user]) + }, [session?.user, isPending]) // Poll machine session periodically to reflect admin changes (e.g., deactivation) useEffect(() => {