sistema-de-chamados/src/components/charts/chart-opened-resolved.tsx
codex-bot a8333c010f fix(reports): remove truncation cap in range collectors to avoid dropped records
feat(calendar): migrate to react-day-picker v9 and polish UI
- Update classNames and CSS import (style.css)
- Custom Dropdown via shadcn Select
- Nav arrows aligned with caption (around)
- Today highlight with cyan tone, weekdays in sentence case
- Wider layout to avoid overflow; remove inner wrapper

chore(tickets): make 'Patrimônio do computador (se houver)' optional
- Backend hotfix to enforce optional + label on existing tenants
- Hide required asterisk for this field in portal/new-ticket

refactor(new-ticket): remove channel dropdown from admin/agent flow
- Keep default channel as MANUAL

feat(ux): simplify requester section and enlarge combobox trigger
- Remove RequesterPreview redundancy; show company badge in trigger
2025-11-04 11:51:08 -03:00

133 lines
5.2 KiB
TypeScript

"use client"
import * as React from "react"
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { useAuth } from "@/lib/auth-client"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { formatDateDM, formatDateDMY } from "@/lib/utils"
import { Skeleton } from "@/components/ui/skeleton"
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
type SeriesPoint = { date: string; opened: number; resolved: number }
const chartConfig = {
opened: { label: "Abertos", color: "var(--chart-1)" },
resolved: { label: "Resolvidos", color: "var(--chart-2)" },
} as const
export function ChartOpenedResolved() {
const [timeRange, setTimeRange] = React.useState("30d")
const [companyId, setCompanyId] = usePersistentCompanyFilter("all")
const { session, convexUserId, isStaff } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const reportsEnabled = Boolean(isStaff && convexUserId)
const data = useQuery(
api.reports.openedResolvedByDay,
reportsEnabled
? ({
tenantId,
viewerId: convexUserId as Id<"users">,
range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
})
: "skip"
) as { rangeDays: number; series: SeriesPoint[] } | undefined
const companies = useQuery(
api.companies.list,
reportsEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as Array<{ id: Id<"companies">; name: string }> | undefined
const companyOptions = React.useMemo<SearchableComboboxOption[]>(() => {
const base: SearchableComboboxOption[] = [{ value: "all", label: "Todas as empresas" }]
if (!companies || companies.length === 0) {
return base
}
const sorted = [...companies].sort((a, b) => a.name.localeCompare(b.name, "pt-BR"))
return [
base[0],
...sorted.map((company) => ({
value: company.id,
label: company.name,
})),
]
}, [companies])
if (!data) {
return <Skeleton className="h-[300px] w-full" />
}
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Abertos x Resolvidos</CardTitle>
<CardDescription>
Evolução diária nos últimos {data.rangeDays} dias
</CardDescription>
<CardAction>
<div className="flex flex-wrap items-center justify-end gap-2 md:gap-3">
<SearchableCombobox
value={companyId}
onValueChange={(next) => setCompanyId(next ?? "all")}
options={companyOptions}
placeholder="Todas as empresas"
className="w-full min-w-56 md:w-56"
/>
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
>
<ToggleGroupItem value="90d">90 dias</ToggleGroupItem>
<ToggleGroupItem value="30d">30 dias</ToggleGroupItem>
<ToggleGroupItem value="7d">7 dias</ToggleGroupItem>
</ToggleGroup>
</div>
</CardAction>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
{data.series.length === 0 ? (
<div className="flex h-[320px] items-center justify-center rounded-xl border border-dashed border-border/60 text-sm text-muted-foreground">
Sem dados suficientes no período selecionado.
</div>
) : (
<ChartContainer config={chartConfig} className="aspect-auto h-[320px] w-full">
<LineChart data={data.series} margin={{ top: 20, left: 16, right: 16, bottom: 12 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(v) => formatDateDM(new Date(v))}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="line"
labelFormatter={(value) => formatDateDMY(new Date(value as string))}
/>
}
/>
<Line dataKey="opened" type="monotone" stroke="var(--color-opened)" strokeWidth={2} dot={{ r: 2 }} strokeLinecap="round" />
<Line dataKey="resolved" type="monotone" stroke="var(--color-resolved)" strokeWidth={2} dot={{ r: 2 }} strokeLinecap="round" />
</LineChart>
</ChartContainer>
)}
</CardContent>
</Card>
)
}