diff --git a/src/app/tickets/[id]/page.tsx b/src/app/tickets/[id]/page.tsx
index 4ed9d6c..b0b7b4f 100644
--- a/src/app/tickets/[id]/page.tsx
+++ b/src/app/tickets/[id]/page.tsx
@@ -2,7 +2,6 @@ import { AppShell } from "@/components/app-shell"
import { SiteHeader } from "@/components/site-header"
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client"
-import { TicketBreadcrumbs } from "@/components/tickets/ticket-breadcrumbs"
import { requireAuthenticatedSession } from "@/lib/auth-server"
type TicketDetailPageProps = {
@@ -19,7 +18,6 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps
}
secondaryAction={}
/>
}
diff --git a/src/components/portal/portal-ticket-detail.tsx b/src/components/portal/portal-ticket-detail.tsx
index d42a490..7dec4a2 100644
--- a/src/components/portal/portal-ticket-detail.tsx
+++ b/src/components/portal/portal-ticket-detail.tsx
@@ -1,6 +1,7 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
+import Link from "next/link"
import { useAction, useMutation, useQuery } from "convex/react"
import { format, formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
@@ -22,6 +23,14 @@ import { Skeleton } from "@/components/ui/skeleton"
// removed wrong import; RichTextEditor comes from rich-text-editor
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { sanitizeEditorHtml, RichTextEditor } from "@/components/ui/rich-text-editor"
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb"
import { TicketStatusBadge } from "@/components/tickets/status-badge"
import { Spinner } from "@/components/ui/spinner"
import { TicketCsatCard } from "@/components/tickets/ticket-csat-card"
@@ -355,6 +364,19 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
return (
+
+
+
+
+ Tickets
+
+
+
+
+ Ticket #{ticket.reference}
+
+
+
diff --git a/src/components/portal/portal-ticket-filters.tsx b/src/components/portal/portal-ticket-filters.tsx
new file mode 100644
index 0000000..1a8fa84
--- /dev/null
+++ b/src/components/portal/portal-ticket-filters.tsx
@@ -0,0 +1,187 @@
+"use client"
+
+import { IconFilter, IconRefresh } from "@tabler/icons-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+
+export type PortalTicketFiltersState = {
+ queue: string | null
+ company: string | null
+ categoryId: string | null
+ assigneeId: string | null
+ status: "active" | "resolved"
+ sort: "recent" | "oldest"
+ dateFrom: string | null
+ dateTo: string | null
+}
+
+export const defaultPortalTicketFilters: PortalTicketFiltersState = {
+ queue: null,
+ company: null,
+ categoryId: null,
+ assigneeId: null,
+ status: "active",
+ sort: "recent",
+ dateFrom: null,
+ dateTo: null,
+}
+
+const ALL_VALUE = "ALL"
+
+type Option = { id: string; name: string }
+
+type PortalTicketFiltersProps = {
+ filters: PortalTicketFiltersState
+ onFiltersChange: (partial: Partial
) => void
+ onReset: () => void
+ queues: string[]
+ companies: string[]
+ categories: Option[]
+ assignees: Option[]
+}
+
+export function PortalTicketFilters({
+ filters,
+ onFiltersChange,
+ onReset,
+ queues,
+ companies,
+ categories,
+ assignees,
+}: PortalTicketFiltersProps) {
+ const handleChange = (partial: Partial) => {
+ onFiltersChange(partial)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A partir de
+
handleChange({ dateFrom: event.target.value || null })}
+ />
+
+
+
Até
+
handleChange({ dateTo: event.target.value || null })}
+ />
+
+
+
+ {(filters.queue || filters.company || filters.categoryId || filters.assigneeId || filters.dateFrom || filters.dateTo || filters.status !== "active" || filters.sort !== "recent") && (
+
+ )}
+
+
+ )
+}
diff --git a/src/components/portal/portal-ticket-list.tsx b/src/components/portal/portal-ticket-list.tsx
index fac7bfe..7d94e96 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 } from "react"
+import { useMemo, useState } from "react"
import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
@@ -14,6 +14,11 @@ import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/
import { Spinner } from "@/components/ui/spinner"
import { Button } from "@/components/ui/button"
import { PortalTicketCard } from "@/components/portal/portal-ticket-card"
+import {
+ PortalTicketFilters,
+ defaultPortalTicketFilters,
+ type PortalTicketFiltersState,
+} from "@/components/portal/portal-ticket-filters"
export function PortalTicketList() {
const { convexUserId, session, machineContext } = useAuth()
@@ -36,6 +41,51 @@ export function PortalTicketList() {
return mapTicketsFromServerList((ticketsRaw as unknown[]) ?? [])
}, [ticketsRaw])
+ const [filters, setFilters] = useState(defaultPortalTicketFilters)
+
+ const queueOptions = useMemo(() => {
+ const set = new Set()
+ ;(tickets as Ticket[]).forEach((ticket) => {
+ if (ticket.queue) {
+ set.add(ticket.queue)
+ }
+ })
+ return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR"))
+ }, [tickets])
+
+ const companyOptions = useMemo(() => {
+ const set = new Set()
+ ;(tickets as Ticket[]).forEach((ticket) => {
+ const name = ticket.company?.name
+ if (name) set.add(name)
+ })
+ return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR"))
+ }, [tickets])
+
+ const categoryOptions = useMemo(() => {
+ const map = new Map()
+ ;(tickets as Ticket[]).forEach((ticket) => {
+ if (ticket.category?.id && ticket.category.name) {
+ map.set(ticket.category.id, ticket.category.name)
+ }
+ })
+ return Array.from(map.entries())
+ .map(([id, name]) => ({ id, name }))
+ .sort((a, b) => a.name.localeCompare(b.name, "pt-BR"))
+ }, [tickets])
+
+ const assigneeOptions = useMemo(() => {
+ const map = new Map()
+ ;(tickets as Ticket[]).forEach((ticket) => {
+ if (ticket.assignee?.id && ticket.assignee.name) {
+ map.set(ticket.assignee.id, ticket.assignee.name)
+ }
+ })
+ return Array.from(map.entries())
+ .map(([id, name]) => ({ id, name }))
+ .sort((a, b) => a.name.localeCompare(b.name, "pt-BR"))
+ }, [tickets])
+
const lastResolvedNoCsat = useMemo(() => {
const resolved = (tickets as Ticket[])
.filter((t) => t.status === "RESOLVED" && (t.csatScore == null))
@@ -43,6 +93,55 @@ export function PortalTicketList() {
return resolved[0] ?? null
}, [tickets])
+ const parseDateInput = (value: string | null, options?: { endOfDay?: boolean }) => {
+ if (!value) return null
+ const [year, month, day] = value.split("-").map((part) => Number(part))
+ if (!year || !month || !day) return null
+ const date = new Date(
+ year,
+ month - 1,
+ day,
+ options?.endOfDay ? 23 : 0,
+ options?.endOfDay ? 59 : 0,
+ options?.endOfDay ? 59 : 0,
+ options?.endOfDay ? 999 : 0
+ )
+ if (Number.isNaN(date.getTime())) return null
+ return date
+ }
+
+ const filteredTickets = useMemo(() => {
+ const fromDate = parseDateInput(filters.dateFrom)
+ const toDate = parseDateInput(filters.dateTo, { endOfDay: true })
+ return (tickets as Ticket[])
+ .filter((ticket) => {
+ if (filters.queue && ticket.queue !== filters.queue) return false
+ if (filters.company && ticket.company?.name !== filters.company) return false
+ if (filters.categoryId && ticket.category?.id !== filters.categoryId) return false
+ if (filters.assigneeId && ticket.assignee?.id !== filters.assigneeId) return false
+ const isResolved = ticket.status === "RESOLVED"
+ if (filters.status === "active" && isResolved) return false
+ if (filters.status === "resolved" && !isResolved) return false
+ const createdAt = ticket.createdAt instanceof Date ? ticket.createdAt : new Date(ticket.createdAt)
+ if (fromDate && createdAt < fromDate) return false
+ if (toDate && createdAt > toDate) return false
+ return true
+ })
+ .sort((a, b) => {
+ const aDate = a.createdAt instanceof Date ? a.createdAt.getTime() : new Date(a.createdAt).getTime()
+ const bDate = b.createdAt instanceof Date ? b.createdAt.getTime() : new Date(b.createdAt).getTime()
+ return filters.sort === "oldest" ? aDate - bDate : bDate - aDate
+ })
+ }, [filters, tickets])
+
+ const handleFiltersChange = (partial: Partial) => {
+ setFilters((prev) => ({ ...prev, ...partial }))
+ }
+
+ const handleResetFilters = () => {
+ setFilters(defaultPortalTicketFilters)
+ }
+
const isLoading = Boolean(viewerId && ticketsRaw === undefined)
if (isLoading) {
@@ -107,11 +206,34 @@ export function PortalTicketList() {
) : null}
-
- {(tickets as Ticket[]).map((ticket) => (
-
- ))}
-
+
+ {filteredTickets.length === 0 ? (
+
+
+ Nenhum ticket corresponde aos filtros
+
+ Ajuste as opções acima ou limpe os filtros para visualizar novamente.
+
+
+
+
+ ) : (
+
+ {filteredTickets.map((ticket) => (
+
+ ))}
+
+ )}
)
}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 1722403..287d515 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -10,7 +10,6 @@ import { cn } from "@/lib/utils"
interface SiteHeaderProps {
title: string
lead?: string
- breadcrumbs?: ReactNode
primaryAction?: ReactNode
secondaryAction?: ReactNode
primaryAlignment?: "right" | "center"
@@ -19,7 +18,6 @@ interface SiteHeaderProps {
function SiteHeaderBase({
title,
lead,
- breadcrumbs,
primaryAction,
secondaryAction,
primaryAlignment = "right",
@@ -34,7 +32,6 @@ function SiteHeaderBase({
- {breadcrumbs ?
{breadcrumbs}
: null}
{lead ?
{lead} : null}
{title}
diff --git a/src/components/tickets/ticket-breadcrumbs.tsx b/src/components/tickets/ticket-breadcrumbs.tsx
deleted file mode 100644
index d97fbe5..0000000
--- a/src/components/tickets/ticket-breadcrumbs.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import Link from "next/link"
-
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
-
-type TicketBreadcrumbsProps = {
- ticketId: string
-}
-
-export function TicketBreadcrumbs({ ticketId }: TicketBreadcrumbsProps) {
- return (
-
-
-
-
- Tickets
-
-
-
-
- Ticket #{ticketId}
-
-
-
- )
-}