From 50a80f52443002fcafe17aee493870a7461c5e90 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Fri, 14 Nov 2025 13:08:59 -0300 Subject: [PATCH] feat(portal): enable ticket reopen and improve loading UX --- .gitignore | 3 +++ .../portal/portal-ticket-detail.tsx | 17 ++++++++++++++- src/components/portal/portal-ticket-list.tsx | 21 +++++++++++-------- src/lib/mappers/ticket.ts | 9 ++++++++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index bc5dc25..14cadee 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ Screenshot*.png *:\:Zone.Identifier # Infrastructure secrets .ci.env + +# ferramentas externas +rustdesk/ diff --git a/src/components/portal/portal-ticket-detail.tsx b/src/components/portal/portal-ticket-detail.tsx index be219e5..ff4b319 100644 --- a/src/components/portal/portal-ticket-detail.tsx +++ b/src/components/portal/portal-ticket-detail.tsx @@ -276,6 +276,10 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) { ) const viewerId = convexUserId ?? null + const viewerRole = role ?? "" + const viewerEmailRaw = session?.user?.email ?? machineContext?.assignedUserEmail ?? null + const viewerEmail = (viewerEmailRaw ?? "").trim().toLowerCase() + const rawReopenDeadline = ticket?.reopenDeadline ?? null const DEFAULT_REOPEN_DAYS = 7 @@ -285,8 +289,19 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) { : fallbackClosedMs ? fallbackClosedMs + DEFAULT_REOPEN_DAYS * 24 * 60 * 60 * 1000 : null + + const isRequesterById = Boolean(ticket?.requester?.id && viewerId && ticket.requester.id === viewerId) + const isRequesterByEmail = Boolean( + viewerEmail && ticket?.requester?.email && viewerEmail === ticket.requester.email.trim().toLowerCase() + ) + const isRequester = isRequesterById || isRequesterByEmail + const reopenWindowActive = inferredDeadline ? inferredDeadline > Date.now() : true - const canReopenTicket = !!ticket && ticket.status === "RESOLVED" && reopenWindowActive + const canReopenTicket = + !!ticket && + ticket.status === "RESOLVED" && + reopenWindowActive && + (isStaff || viewerRole === "manager" || isRequester) const reopenDeadlineLabel = useMemo(() => { const deadline = inferredDeadline ?? rawReopenDeadline if (!deadline) return null diff --git a/src/components/portal/portal-ticket-list.tsx b/src/components/portal/portal-ticket-list.tsx index 70d21f6..db2f9ca 100644 --- a/src/components/portal/portal-ticket-list.tsx +++ b/src/components/portal/portal-ticket-list.tsx @@ -1,6 +1,6 @@ "use client" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { useQuery } from "convex/react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" @@ -21,7 +21,7 @@ import { } from "@/components/portal/portal-ticket-filters" export function PortalTicketList() { - const { convexUserId, session, machineContext, role, isLoading: authLoading, machineContextLoading } = useAuth() + const { convexUserId, session, machineContext, role } = useAuth() const viewerId = (convexUserId ?? machineContext?.assignedUserId ?? null) as Id<"users"> | null @@ -41,6 +41,15 @@ export function PortalTicketList() { return mapTicketsFromServerList((ticketsRaw as unknown[]) ?? []) }, [ticketsRaw]) + const [initialLoadCompleted, setInitialLoadCompleted] = useState(false) + + useEffect(() => { + if (initialLoadCompleted) return + if (!viewerId) return + if (ticketsRaw === undefined) return + setInitialLoadCompleted(true) + }, [initialLoadCompleted, viewerId, ticketsRaw]) + const [filters, setFilters] = useState(defaultPortalTicketFilters) // No app desktop, colaboradores e gestores não devem ver filtros avançados @@ -145,13 +154,7 @@ export function PortalTicketList() { setFilters(defaultPortalTicketFilters) } - const hasAuthContext = Boolean(session || machineContext) - const isLoading = Boolean( - authLoading || - machineContextLoading || - (hasAuthContext && !viewerId) || - (viewerId && ticketsRaw === undefined) - ) + const isLoading = !initialLoadCompleted if (isLoading) { return ( diff --git a/src/lib/mappers/ticket.ts b/src/lib/mappers/ticket.ts index 5763ce9..80a904c 100644 --- a/src/lib/mappers/ticket.ts +++ b/src/lib/mappers/ticket.ts @@ -113,6 +113,9 @@ const serverTicketSchema = z.object({ dueAt: z.number().nullable().optional(), firstResponseAt: z.number().nullable().optional(), resolvedAt: z.number().nullable().optional(), + reopenDeadline: z.number().nullable().optional(), + reopenWindowDays: z.number().nullable().optional(), + reopenedAt: z.number().nullable().optional(), updatedAt: z.number(), createdAt: z.number(), tags: z.array(z.string()).default([]).optional(), @@ -258,6 +261,9 @@ export function mapTicketFromServer(input: unknown) { dueAt: s.dueAt ? new Date(s.dueAt) : null, firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null, resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null, + reopenDeadline: typeof s.reopenDeadline === "number" ? s.reopenDeadline : null, + reopenWindowDays: typeof s.reopenWindowDays === "number" ? s.reopenWindowDays : null, + reopenedAt: typeof s.reopenedAt === "number" ? s.reopenedAt : null, csatScore: typeof csatScore === "number" ? csatScore : null, csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null, csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null, @@ -356,6 +362,9 @@ export function mapTicketWithDetailsFromServer(input: unknown) { dueAt: base.dueAt ? new Date(base.dueAt) : null, firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null, resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : null, + reopenDeadline: typeof base.reopenDeadline === "number" ? base.reopenDeadline : null, + reopenWindowDays: typeof base.reopenWindowDays === "number" ? base.reopenWindowDays : null, + reopenedAt: typeof base.reopenedAt === "number" ? base.reopenedAt : null, csatScore: typeof csatScore === "number" ? csatScore : null, csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null, csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null,