feat(portal): enable ticket reopen and improve loading UX
This commit is contained in:
parent
8b905dc467
commit
50a80f5244
4 changed files with 40 additions and 10 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -60,3 +60,6 @@ Screenshot*.png
|
||||||
*:\:Zone.Identifier
|
*:\:Zone.Identifier
|
||||||
# Infrastructure secrets
|
# Infrastructure secrets
|
||||||
.ci.env
|
.ci.env
|
||||||
|
|
||||||
|
# ferramentas externas
|
||||||
|
rustdesk/
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,10 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const viewerId = convexUserId ?? null
|
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 rawReopenDeadline = ticket?.reopenDeadline ?? null
|
||||||
|
|
||||||
const DEFAULT_REOPEN_DAYS = 7
|
const DEFAULT_REOPEN_DAYS = 7
|
||||||
|
|
@ -285,8 +289,19 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
|
||||||
: fallbackClosedMs
|
: fallbackClosedMs
|
||||||
? fallbackClosedMs + DEFAULT_REOPEN_DAYS * 24 * 60 * 60 * 1000
|
? fallbackClosedMs + DEFAULT_REOPEN_DAYS * 24 * 60 * 60 * 1000
|
||||||
: null
|
: 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 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 reopenDeadlineLabel = useMemo(() => {
|
||||||
const deadline = inferredDeadline ?? rawReopenDeadline
|
const deadline = inferredDeadline ?? rawReopenDeadline
|
||||||
if (!deadline) return null
|
if (!deadline) return null
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useQuery } from "convex/react"
|
import { useQuery } from "convex/react"
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
import type { Id } from "@/convex/_generated/dataModel"
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from "@/components/portal/portal-ticket-filters"
|
} from "@/components/portal/portal-ticket-filters"
|
||||||
|
|
||||||
export function PortalTicketList() {
|
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
|
const viewerId = (convexUserId ?? machineContext?.assignedUserId ?? null) as Id<"users"> | null
|
||||||
|
|
||||||
|
|
@ -41,6 +41,15 @@ export function PortalTicketList() {
|
||||||
return mapTicketsFromServerList((ticketsRaw as unknown[]) ?? [])
|
return mapTicketsFromServerList((ticketsRaw as unknown[]) ?? [])
|
||||||
}, [ticketsRaw])
|
}, [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)
|
const [filters, setFilters] = useState<PortalTicketFiltersState>(defaultPortalTicketFilters)
|
||||||
|
|
||||||
// No app desktop, colaboradores e gestores não devem ver filtros avançados
|
// No app desktop, colaboradores e gestores não devem ver filtros avançados
|
||||||
|
|
@ -145,13 +154,7 @@ export function PortalTicketList() {
|
||||||
setFilters(defaultPortalTicketFilters)
|
setFilters(defaultPortalTicketFilters)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAuthContext = Boolean(session || machineContext)
|
const isLoading = !initialLoadCompleted
|
||||||
const isLoading = Boolean(
|
|
||||||
authLoading ||
|
|
||||||
machineContextLoading ||
|
|
||||||
(hasAuthContext && !viewerId) ||
|
|
||||||
(viewerId && ticketsRaw === undefined)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,9 @@ const serverTicketSchema = z.object({
|
||||||
dueAt: z.number().nullable().optional(),
|
dueAt: z.number().nullable().optional(),
|
||||||
firstResponseAt: z.number().nullable().optional(),
|
firstResponseAt: z.number().nullable().optional(),
|
||||||
resolvedAt: 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(),
|
updatedAt: z.number(),
|
||||||
createdAt: z.number(),
|
createdAt: z.number(),
|
||||||
tags: z.array(z.string()).default([]).optional(),
|
tags: z.array(z.string()).default([]).optional(),
|
||||||
|
|
@ -258,6 +261,9 @@ export function mapTicketFromServer(input: unknown) {
|
||||||
dueAt: s.dueAt ? new Date(s.dueAt) : null,
|
dueAt: s.dueAt ? new Date(s.dueAt) : null,
|
||||||
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
|
||||||
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : 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,
|
csatScore: typeof csatScore === "number" ? csatScore : null,
|
||||||
csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null,
|
csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null,
|
||||||
csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : 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,
|
dueAt: base.dueAt ? new Date(base.dueAt) : null,
|
||||||
firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null,
|
firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null,
|
||||||
resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : 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,
|
csatScore: typeof csatScore === "number" ? csatScore : null,
|
||||||
csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null,
|
csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null,
|
||||||
csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null,
|
csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue