feat(filters): ticket company filter + column; reports: company filter in CSVs; dashboard: queue summary; docs: agents.md and roadmap updates
This commit is contained in:
parent
70f91f5bbd
commit
2cf399dcb1
9 changed files with 100 additions and 31 deletions
|
|
@ -57,15 +57,18 @@ export async function GET(request: Request) {
|
|||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const range = searchParams.get("range") ?? undefined
|
||||
const companyId = searchParams.get("companyId") ?? undefined
|
||||
const report = await client.query(api.reports.backlogOverview, {
|
||||
tenantId,
|
||||
viewerId: viewerId as unknown as Id<"users">,
|
||||
range,
|
||||
companyId: companyId as any,
|
||||
})
|
||||
|
||||
const rows: Array<Array<unknown>> = []
|
||||
rows.push(["Relatório", "Backlog"])
|
||||
rows.push(["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : "—"])
|
||||
if (companyId) rows.push(["EmpresaId", companyId])
|
||||
rows.push([])
|
||||
rows.push(["Seção", "Chave", "Valor"]) // header
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export async function GET(request: Request) {
|
|||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const range = searchParams.get("range") ?? undefined // "7d" | "30d" | undefined(=90d)
|
||||
const companyId = searchParams.get("companyId") ?? undefined
|
||||
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
|
@ -62,6 +63,7 @@ export async function GET(request: Request) {
|
|||
tenantId,
|
||||
viewerId: viewerId as unknown as Id<"users">,
|
||||
range,
|
||||
companyId: companyId as any,
|
||||
})
|
||||
|
||||
const channels = report.channels
|
||||
|
|
@ -91,7 +93,7 @@ export async function GET(request: Request) {
|
|||
return new NextResponse(csv, {
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=UTF-8",
|
||||
"Content-Disposition": `attachment; filename="tickets-by-channel-${tenantId}-${range ?? '90d'}.csv"`,
|
||||
"Content-Disposition": `attachment; filename="tickets-by-channel-${tenantId}-${range ?? '90d'}${companyId ? `-${companyId}` : ''}.csv"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import { AppShell } from "@/components/app-shell"
|
|||
import { SectionCards } from "@/components/section-cards"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { RecentTicketsPanel } from "@/components/tickets/recent-tickets-panel"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const TicketQueueSummaryCards = dynamic(
|
||||
() => import("@/components/tickets/ticket-queue-summary").then((m) => ({ default: m.TicketQueueSummaryCards })),
|
||||
{ ssr: false }
|
||||
)
|
||||
import { ChartAreaInteractive } from "@/components/chart-area-interactive"
|
||||
|
||||
export default function Dashboard() {
|
||||
|
|
@ -18,9 +24,12 @@ export default function Dashboard() {
|
|||
>
|
||||
<SectionCards />
|
||||
<div className="grid gap-6 px-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] lg:px-6 lg:[&>*]:min-w-0">
|
||||
<ChartAreaInteractive />
|
||||
<ChartAreaInteractive />
|
||||
<RecentTicketsPanel />
|
||||
</div>
|
||||
<div className="px-4 lg:px-6">
|
||||
<TicketQueueSummaryCards />
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export type TicketFiltersState = {
|
|||
priority: string | null
|
||||
queue: string | null
|
||||
channel: string | null
|
||||
company: string | null
|
||||
view: "active" | "completed"
|
||||
}
|
||||
|
||||
|
|
@ -75,17 +76,19 @@ export const defaultTicketFilters: TicketFiltersState = {
|
|||
priority: null,
|
||||
queue: null,
|
||||
channel: null,
|
||||
company: null,
|
||||
view: "active",
|
||||
}
|
||||
|
||||
interface TicketsFiltersProps {
|
||||
onChange?: (filters: TicketFiltersState) => void
|
||||
queues?: QueueOption[]
|
||||
companies?: string[]
|
||||
}
|
||||
|
||||
const ALL_VALUE = "ALL"
|
||||
|
||||
export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) {
|
||||
export function TicketsFilters({ onChange, queues = [], companies = [] }: TicketsFiltersProps) {
|
||||
const [filters, setFilters] = useState<TicketFiltersState>(defaultTicketFilters)
|
||||
|
||||
function setPartial(partial: Partial<TicketFiltersState>) {
|
||||
|
|
@ -103,6 +106,7 @@ export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) {
|
|||
if (filters.priority) chips.push(`Prioridade: ${filters.priority}`)
|
||||
if (filters.queue) chips.push(`Fila: ${filters.queue}`)
|
||||
if (filters.channel) chips.push(`Canal: ${filters.channel}`)
|
||||
if (filters.company) chips.push(`Empresa: ${filters.company}`)
|
||||
if (!filters.status && filters.view === "completed") chips.push("Exibindo concluídos")
|
||||
return chips
|
||||
}, [filters])
|
||||
|
|
@ -133,6 +137,22 @@ export function TicketsFilters({ onChange, queues = [] }: TicketsFiltersProps) {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={filters.company ?? ALL_VALUE}
|
||||
onValueChange={(value) => setPartial({ company: value === ALL_VALUE ? null : value })}
|
||||
>
|
||||
<SelectTrigger className="md:w-[220px]">
|
||||
<SelectValue placeholder="Empresa" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_VALUE}>Todas as empresas</SelectItem>
|
||||
{companies.map((company) => (
|
||||
<SelectItem key={company!} value={company!}>
|
||||
{company}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
<TableHead className="hidden w-[120px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 lg:table-cell">
|
||||
Fila
|
||||
</TableHead>
|
||||
<TableHead className="hidden w-[180px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 lg:table-cell">
|
||||
Empresa
|
||||
</TableHead>
|
||||
<TableHead className="hidden w-[80px] pl-1 pr-3 py-3 text-left text-[11px] font-semibold uppercase tracking-wide text-neutral-600 first:pl-6 last:pr-6 md:table-cell">
|
||||
Canal
|
||||
</TableHead>
|
||||
|
|
@ -231,6 +234,11 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {
|
|||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden lg:table-cell pl-1`}>
|
||||
<span className="text-sm text-neutral-800">
|
||||
{(ticket as any).company?.name ?? "—"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden md:table-cell pl-1 pr-8`}>
|
||||
<div
|
||||
className="inline-flex"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ export function TicketsView() {
|
|||
)
|
||||
|
||||
const tickets = useMemo(() => mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]), [ticketsRaw])
|
||||
const companies = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
for (const t of tickets) {
|
||||
const name = (t as any).company?.name as string | undefined
|
||||
if (name) set.add(name)
|
||||
}
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR"))
|
||||
}, [tickets])
|
||||
|
||||
const filteredTickets = useMemo(() => {
|
||||
const completedStatuses = new Set<Ticket["status"]>(["RESOLVED"])
|
||||
|
|
@ -55,13 +63,16 @@ export function TicketsView() {
|
|||
if (filters.queue) {
|
||||
working = working.filter((t) => t.queue === filters.queue)
|
||||
}
|
||||
if (filters.company) {
|
||||
working = working.filter((t) => ((t as any).company?.name ?? null) === filters.company)
|
||||
}
|
||||
|
||||
return working
|
||||
}, [tickets, filters.queue, filters.status, filters.view])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
||||
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q) => q.name)} />
|
||||
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q) => q.name)} companies={companies} />
|
||||
{ticketsRaw === undefined ? (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div className="grid gap-3">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue