Adjust ticket filters visibility and add date range
This commit is contained in:
parent
5f7ef3fd03
commit
cc68c85246
3 changed files with 110 additions and 91 deletions
|
|
@ -21,6 +21,7 @@ function getParamValue(value: string | string[] | undefined): string | undefined
|
||||||
|
|
||||||
function deriveInitialFilters(params: Record<string, string | string[] | undefined>): Partial<TicketFiltersState> {
|
function deriveInitialFilters(params: Record<string, string | string[] | undefined>): Partial<TicketFiltersState> {
|
||||||
const initial: Partial<TicketFiltersState> = {}
|
const initial: Partial<TicketFiltersState> = {}
|
||||||
|
const isValidDateParam = (value?: string) => Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value))
|
||||||
const view = getParamValue(params.view)
|
const view = getParamValue(params.view)
|
||||||
if (view === "completed" || view === "active") {
|
if (view === "completed" || view === "active") {
|
||||||
initial.view = view
|
initial.view = view
|
||||||
|
|
@ -41,10 +42,10 @@ function deriveInitialFilters(params: Record<string, string | string[] | undefin
|
||||||
if (assigneeId) initial.assigneeId = assigneeId
|
if (assigneeId) initial.assigneeId = assigneeId
|
||||||
const categoryId = getParamValue(params.category)
|
const categoryId = getParamValue(params.category)
|
||||||
if (categoryId) initial.categoryId = categoryId
|
if (categoryId) initial.categoryId = categoryId
|
||||||
const dateFrom = getParamValue(params.from)
|
const from = getParamValue(params.from ?? params.dateFrom)
|
||||||
if (dateFrom) initial.dateFrom = dateFrom
|
if (isValidDateParam(from)) initial.dateFrom = from
|
||||||
const dateTo = getParamValue(params.to)
|
const to = getParamValue(params.to ?? params.dateTo)
|
||||||
if (dateTo) initial.dateTo = dateTo
|
if (isValidDateParam(to)) initial.dateTo = to
|
||||||
const sort = getParamValue(params.sort)
|
const sort = getParamValue(params.sort)
|
||||||
if (sort === "recent" || sort === "oldest") {
|
if (sort === "recent" || sort === "oldest") {
|
||||||
initial.sort = sort as TicketFiltersState["sort"]
|
initial.sort = sort as TicketFiltersState["sort"]
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export { defaultTicketFilters }
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
|
@ -28,6 +29,21 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
import { DatePicker } from "@/components/ui/date-picker"
|
||||||
|
|
||||||
|
type QueueOption = string
|
||||||
|
|
||||||
|
interface TicketsFiltersProps {
|
||||||
|
onChange?: (filters: TicketFiltersState) => void
|
||||||
|
queues?: QueueOption[]
|
||||||
|
companies?: string[]
|
||||||
|
assignees?: Array<{ id: string; name: string }>
|
||||||
|
categories?: Array<{ id: string; name: string }>
|
||||||
|
initialState?: Partial<TicketFiltersState>
|
||||||
|
viewerRole?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_VALUE = "ALL"
|
||||||
|
|
||||||
const statusOptions: Array<{ value: TicketStatus; label: string }> = [
|
const statusOptions: Array<{ value: TicketStatus; label: string }> = [
|
||||||
{ value: "PENDING", label: "Pendente" },
|
{ value: "PENDING", label: "Pendente" },
|
||||||
|
|
@ -63,19 +79,6 @@ const channelOptions = ticketChannelSchema.options.map((channel) => ({
|
||||||
}[channel],
|
}[channel],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
type QueueOption = string
|
|
||||||
|
|
||||||
interface TicketsFiltersProps {
|
|
||||||
onChange?: (filters: TicketFiltersState) => void
|
|
||||||
queues?: QueueOption[]
|
|
||||||
companies?: string[]
|
|
||||||
assignees?: Array<{ id: string; name: string }>
|
|
||||||
categories?: Array<{ id: string; name: string }>
|
|
||||||
initialState?: Partial<TicketFiltersState>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALL_VALUE = "ALL"
|
|
||||||
|
|
||||||
export function TicketsFilters({
|
export function TicketsFilters({
|
||||||
onChange,
|
onChange,
|
||||||
queues = [],
|
queues = [],
|
||||||
|
|
@ -83,6 +86,7 @@ export function TicketsFilters({
|
||||||
assignees = [],
|
assignees = [],
|
||||||
categories = [],
|
categories = [],
|
||||||
initialState,
|
initialState,
|
||||||
|
viewerRole,
|
||||||
}: TicketsFiltersProps) {
|
}: TicketsFiltersProps) {
|
||||||
const mergedDefaults = useMemo(
|
const mergedDefaults = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -101,19 +105,21 @@ export function TicketsFilters({
|
||||||
setFilters((prev) => ({ ...prev, ...partial }))
|
setFilters((prev) => ({ ...prev, ...partial }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propaga as mudancas de filtros para o componente pai sem disparar durante a renderizacao
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange?.(filters)
|
onChange?.(filters)
|
||||||
}, [filters, onChange])
|
}, [filters, onChange])
|
||||||
|
|
||||||
|
const normalizedRole = viewerRole?.toLowerCase() ?? null
|
||||||
|
const canUseAdvancedFilters = normalizedRole === "admin" || normalizedRole === "agent"
|
||||||
|
|
||||||
const activeFilters = useMemo(() => {
|
const activeFilters = useMemo(() => {
|
||||||
const chips: string[] = []
|
const chips: string[] = []
|
||||||
if (filters.status) chips.push(`Status: ${statusLabelMap[filters.status] ?? filters.status}`)
|
if (filters.status) chips.push(`Status: ${statusLabelMap[filters.status] ?? filters.status}`)
|
||||||
if (filters.priority) chips.push(`Prioridade: ${filters.priority}`)
|
if (filters.priority) chips.push(`Prioridade: ${filters.priority}`)
|
||||||
if (filters.queue) chips.push(`Fila: ${filters.queue}`)
|
if (filters.queue && canUseAdvancedFilters) chips.push(`Fila: ${filters.queue}`)
|
||||||
if (filters.channel) chips.push(`Canal: ${filters.channel}`)
|
if (filters.channel) chips.push(`Canal: ${filters.channel}`)
|
||||||
if (filters.company) chips.push(`Empresa: ${filters.company}`)
|
if (filters.company && canUseAdvancedFilters) chips.push(`Empresa: ${filters.company}`)
|
||||||
if (filters.assigneeId) {
|
if (filters.assigneeId && canUseAdvancedFilters) {
|
||||||
const found = assignees.find((a) => a.id === filters.assigneeId)
|
const found = assignees.find((a) => a.id === filters.assigneeId)
|
||||||
chips.push(`Responsável: ${found?.name ?? filters.assigneeId}`)
|
chips.push(`Responsável: ${found?.name ?? filters.assigneeId}`)
|
||||||
}
|
}
|
||||||
|
|
@ -126,50 +132,54 @@ export function TicketsFilters({
|
||||||
if (filters.dateFrom || filters.dateTo) chips.push(formatDateRangeChip(filters.dateFrom, filters.dateTo))
|
if (filters.dateFrom || filters.dateTo) chips.push(formatDateRangeChip(filters.dateFrom, filters.dateTo))
|
||||||
if (filters.sort === "oldest") chips.push("Ordenados por mais antigos")
|
if (filters.sort === "oldest") chips.push("Ordenados por mais antigos")
|
||||||
return chips
|
return chips
|
||||||
}, [filters, assignees, categories])
|
}, [filters, assignees, categories, canUseAdvancedFilters])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="flex flex-1 flex-col gap-2 md:flex-row">
|
<div className="flex flex-1 flex-col gap-2 md:flex-row md:flex-wrap">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Buscar por assunto ou #ID"
|
placeholder="Buscar por assunto ou #ID"
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={(event) => setPartial({ search: event.target.value })}
|
onChange={(event) => setPartial({ search: event.target.value })}
|
||||||
className="md:max-w-sm"
|
className="md:max-w-sm"
|
||||||
/>
|
/>
|
||||||
<Select
|
{canUseAdvancedFilters ? (
|
||||||
value={filters.queue ?? ALL_VALUE}
|
<Select
|
||||||
onValueChange={(value) => setPartial({ queue: value === ALL_VALUE ? null : value })}
|
value={filters.queue ?? ALL_VALUE}
|
||||||
>
|
onValueChange={(value) => setPartial({ queue: value === ALL_VALUE ? null : value })}
|
||||||
<SelectTrigger className="md:w-[180px]">
|
>
|
||||||
<SelectValue placeholder="Fila" />
|
<SelectTrigger className="md:w-[180px]">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Fila" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value={ALL_VALUE}>Todas as filas</SelectItem>
|
<SelectContent>
|
||||||
{queues.map((queue) => (
|
<SelectItem value={ALL_VALUE}>Todas as filas</SelectItem>
|
||||||
<SelectItem key={queue!} value={queue!}>
|
{queues.map((queue) => (
|
||||||
{queue}
|
<SelectItem key={queue!} value={queue!}>
|
||||||
</SelectItem>
|
{queue}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<Select
|
</Select>
|
||||||
value={filters.company ?? ALL_VALUE}
|
) : null}
|
||||||
onValueChange={(value) => setPartial({ company: value === ALL_VALUE ? null : value })}
|
{canUseAdvancedFilters ? (
|
||||||
>
|
<Select
|
||||||
<SelectTrigger className="md:w-[220px]">
|
value={filters.company ?? ALL_VALUE}
|
||||||
<SelectValue placeholder="Empresa" />
|
onValueChange={(value) => setPartial({ company: value === ALL_VALUE ? null : value })}
|
||||||
</SelectTrigger>
|
>
|
||||||
<SelectContent>
|
<SelectTrigger className="md:w-[220px]">
|
||||||
<SelectItem value={ALL_VALUE}>Todas as empresas</SelectItem>
|
<SelectValue placeholder="Empresa" />
|
||||||
{companies.map((company) => (
|
</SelectTrigger>
|
||||||
<SelectItem key={company!} value={company!}>
|
<SelectContent>
|
||||||
{company}
|
<SelectItem value={ALL_VALUE}>Todas as empresas</SelectItem>
|
||||||
</SelectItem>
|
{companies.map((company) => (
|
||||||
))}
|
<SelectItem key={company!} value={company!}>
|
||||||
</SelectContent>
|
{company}
|
||||||
</Select>
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : null}
|
||||||
<Select
|
<Select
|
||||||
value={filters.categoryId ?? ALL_VALUE}
|
value={filters.categoryId ?? ALL_VALUE}
|
||||||
onValueChange={(value) => setPartial({ categoryId: value === ALL_VALUE ? null : value })}
|
onValueChange={(value) => setPartial({ categoryId: value === ALL_VALUE ? null : value })}
|
||||||
|
|
@ -187,23 +197,25 @@ export function TicketsFilters({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Select
|
{canUseAdvancedFilters ? (
|
||||||
value={filters.assigneeId ?? ALL_VALUE}
|
<Select
|
||||||
onValueChange={(value) => setPartial({ assigneeId: value === ALL_VALUE ? null : value })}
|
value={filters.assigneeId ?? ALL_VALUE}
|
||||||
>
|
onValueChange={(value) => setPartial({ assigneeId: value === ALL_VALUE ? null : value })}
|
||||||
<SelectTrigger className="md:w-[220px]">
|
>
|
||||||
<SelectValue placeholder="Responsável" />
|
<SelectTrigger className="md:w-[220px]">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Responsável" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value={ALL_VALUE}>Todos os responsáveis</SelectItem>
|
<SelectContent>
|
||||||
{assignees.map((user) => (
|
<SelectItem value={ALL_VALUE}>Todos os responsáveis</SelectItem>
|
||||||
<SelectItem key={user.id} value={user.id}>
|
{assignees.map((user) => (
|
||||||
{user.name}
|
<SelectItem key={user.id} value={user.id}>
|
||||||
</SelectItem>
|
{user.name}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : null}
|
||||||
<Select
|
<Select
|
||||||
value={filters.view}
|
value={filters.view}
|
||||||
onValueChange={(value) => setPartial({ view: value as TicketFiltersState["view"] })}
|
onValueChange={(value) => setPartial({ view: value as TicketFiltersState["view"] })}
|
||||||
|
|
@ -305,23 +317,6 @@ export function TicketsFilters({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-semibold uppercase text-neutral-500">
|
|
||||||
Período
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={filters.dateFrom ?? ""}
|
|
||||||
onChange={(event) => setPartial({ dateFrom: event.target.value || null })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={filters.dateTo ?? ""}
|
|
||||||
onChange={(event) => setPartial({ dateTo: event.target.value || null })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -335,6 +330,29 @@ export function TicketsFilters({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white/70 p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Período</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-3">
|
||||||
|
<div className="flex min-w-[200px] flex-1 flex-col gap-1">
|
||||||
|
<Label className="text-xs font-semibold uppercase tracking-wide text-neutral-500">A partir de</Label>
|
||||||
|
<DatePicker
|
||||||
|
value={filters.dateFrom}
|
||||||
|
onChange={(value) => setPartial({ dateFrom: value })}
|
||||||
|
placeholder="Selecionar data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-[200px] flex-1 flex-col gap-1">
|
||||||
|
<Label className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Até</Label>
|
||||||
|
<DatePicker
|
||||||
|
value={filters.dateTo}
|
||||||
|
onChange={(value) => setPartial({ dateTo: value })}
|
||||||
|
placeholder="Selecionar data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{activeFilters.length > 0 && (
|
{activeFilters.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{activeFilters.map((chip) => (
|
{activeFilters.map((chip) => (
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
|
||||||
setFilters(mergedInitialFilters)
|
setFilters(mergedInitialFilters)
|
||||||
}, [mergedInitialFilters])
|
}, [mergedInitialFilters])
|
||||||
|
|
||||||
const { session, convexUserId, isStaff } = useAuth()
|
const { session, convexUserId, isStaff, role } = useAuth()
|
||||||
const userId = session?.user?.id ?? null
|
const userId = session?.user?.id ?? null
|
||||||
const tenantId = session?.user?.tenantId ?? DEFAULT_TENANT_ID
|
const tenantId = session?.user?.tenantId ?? DEFAULT_TENANT_ID
|
||||||
const viewModeStorageKey = useMemo(() => {
|
const viewModeStorageKey = useMemo(() => {
|
||||||
|
|
@ -111,7 +111,6 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// load saved filters as defaults per user
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!convexUserId) return
|
if (!convexUserId) return
|
||||||
try {
|
try {
|
||||||
|
|
@ -230,6 +229,7 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
|
||||||
assignees={(agents ?? []).map((a) => ({ id: a._id, name: a.name }))}
|
assignees={(agents ?? []).map((a) => ({ id: a._id, name: a.name }))}
|
||||||
categories={ticketCategories.map((category) => ({ id: category.id, name: category.name }))}
|
categories={ticketCategories.map((category) => ({ id: category.id, name: category.name }))}
|
||||||
initialState={mergedInitialFilters}
|
initialState={mergedInitialFilters}
|
||||||
|
viewerRole={role}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue