feat(portal): enable ticket reopen and improve loading UX

This commit is contained in:
Esdras Renan 2025-11-14 13:08:59 -03:00
parent 8b905dc467
commit 50a80f5244
4 changed files with 40 additions and 10 deletions

View file

@ -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

View file

@ -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<PortalTicketFiltersState>(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 (

View file

@ -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,