Hours by client: add search and CSV filtering; add alerts cron (BRT 08:00 guard) + alerts panel filters; admin companies shows last alert; PDF Inter font from public/fonts; fix Select empty value; type cleanups; tests for CSV/TZ; remove Knowledge Base nav
This commit is contained in:
parent
2cf399dcb1
commit
08cc8037d5
151 changed files with 1404 additions and 214 deletions
|
|
@ -25,13 +25,14 @@ import {
|
|||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
|
|
@ -43,6 +44,9 @@ export function ChartAreaInteractive() {
|
|||
const [mounted, setMounted] = React.useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("7d")
|
||||
// Use a non-empty sentinel value for "all" to satisfy Select.Item requirements
|
||||
const [companyId, setCompanyId] = React.useState<string>("all")
|
||||
const [companyQuery, setCompanyQuery] = React.useState("")
|
||||
const { session, convexUserId } = useAuth()
|
||||
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
|
|
@ -59,9 +63,15 @@ export function ChartAreaInteractive() {
|
|||
const report = useQuery(
|
||||
api.reports.ticketsByChannel,
|
||||
convexUserId
|
||||
? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange })
|
||||
? ({ tenantId, viewerId: convexUserId as Id<"users">, range: timeRange, companyId: companyId === "all" ? undefined : (companyId as Id<"companies">) })
|
||||
: "skip"
|
||||
)
|
||||
const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined
|
||||
const filteredCompanies = React.useMemo(() => {
|
||||
const q = companyQuery.trim().toLowerCase()
|
||||
if (!q) return companies ?? []
|
||||
return (companies ?? []).filter((c) => c.name.toLowerCase().includes(q))
|
||||
}, [companies, companyQuery])
|
||||
|
||||
const channels = React.useMemo(() => report?.channels ?? [], [report])
|
||||
|
||||
|
|
@ -120,46 +130,68 @@ export function ChartAreaInteractive() {
|
|||
<span className="@[540px]/card:hidden">Período: {timeRange}</span>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<a
|
||||
href={`/api/reports/tickets-by-channel.csv?range=${timeRange}`}
|
||||
download
|
||||
<div className="flex w-full flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-end sm:gap-2">
|
||||
{/* Company picker with search */}
|
||||
<Select value={companyId} onValueChange={(v) => { setCompanyId(v); }}>
|
||||
<SelectTrigger className="w-full min-w-56 sm:w-64">
|
||||
<SelectValue placeholder="Todas as empresas" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Pesquisar empresa..."
|
||||
value={companyQuery}
|
||||
onChange={(e) => setCompanyQuery(e.target.value)}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<SelectItem value="all">Todas as empresas</SelectItem>
|
||||
{filteredCompanies.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Desktop time range toggles */}
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
>
|
||||
Exportar CSV
|
||||
</a>
|
||||
</Button>
|
||||
<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>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger
|
||||
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
||||
size="sm"
|
||||
aria-label="Selecionar período"
|
||||
>
|
||||
<SelectValue placeholder="Selecionar período" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="90d" className="rounded-lg">
|
||||
Últimos 90 dias
|
||||
</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">
|
||||
Últimos 30 dias
|
||||
</SelectItem>
|
||||
<SelectItem value="7d" className="rounded-lg">
|
||||
Últimos 7 dias
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardAction>
|
||||
<ToggleGroupItem value="90d">90 dias</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">30 dias</ToggleGroupItem>
|
||||
<ToggleGroupItem value="7d">7 dias</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{/* Mobile time range select */}
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger
|
||||
className="flex w-full min-w-40 @[767px]/card:hidden"
|
||||
size="sm"
|
||||
aria-label="Selecionar período"
|
||||
>
|
||||
<SelectValue placeholder="Selecionar período" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="90d" className="rounded-lg">Últimos 90 dias</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">Últimos 30 dias</SelectItem>
|
||||
<SelectItem value="7d" className="rounded-lg">Últimos 7 dias</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Export button aligned at the end */}
|
||||
<Button asChild size="sm" variant="outline" className="sm:ml-1">
|
||||
<a
|
||||
href={`/api/reports/tickets-by-channel.csv?range=${timeRange}${companyId !== "all" ? `&companyId=${companyId}` : ""}`}
|
||||
download
|
||||
>
|
||||
Exportar CSV
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
{report === undefined ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue