feat(reports): adiciona opcao Todas as empresas no relatorio por empresa

- Frontend: usa usePersistentCompanyFilter para persistir selecao
- Frontend: adiciona opcao "Todas as empresas" como primeira opcao
- Backend: torna companyId opcional na query companyOverview
- Backend: usa resolveScopedCompanyId para scoping de gestores

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-17 19:01:56 -03:00
parent 14480df9f3
commit 8a237a820d
2 changed files with 59 additions and 39 deletions

View file

@ -2406,18 +2406,20 @@ export const companyOverview = query({
args: { args: {
tenantId: v.string(), tenantId: v.string(),
viewerId: v.id("users"), viewerId: v.id("users"),
companyId: v.id("companies"), companyId: v.optional(v.id("companies")),
range: v.optional(v.string()), range: v.optional(v.string()),
}, },
handler: async (ctx, { tenantId, viewerId, companyId, range }) => { handler: async (ctx, { tenantId, viewerId, companyId, range }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId); const viewer = await requireStaff(ctx, viewerId, tenantId);
if (viewer.role === "MANAGER" && viewer.user.companyId && viewer.user.companyId !== companyId) { const scopedCompanyId = resolveScopedCompanyId(viewer, companyId);
throw new ConvexError("Gestores só podem consultar relatórios da própria empresa");
}
const company = await ctx.db.get(companyId); // Buscar dados da empresa selecionada (se houver)
if (!company || company.tenantId !== tenantId) { let company: Doc<"companies"> | null = null;
throw new ConvexError("Empresa não encontrada"); if (scopedCompanyId) {
company = await ctx.db.get(scopedCompanyId);
if (!company || company.tenantId !== tenantId) {
throw new ConvexError("Empresa não encontrada");
}
} }
const normalizedRange = (range ?? "30d").toLowerCase(); const normalizedRange = (range ?? "30d").toLowerCase();
@ -2426,20 +2428,35 @@ export const companyOverview = query({
const startMs = now - rangeDays * ONE_DAY_MS; const startMs = now - rangeDays * ONE_DAY_MS;
// Limita consultas para evitar OOM em empresas muito grandes // Limita consultas para evitar OOM em empresas muito grandes
const tickets = await ctx.db const tickets = scopedCompanyId
.query("tickets") ? await ctx.db
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .query("tickets")
.take(2000); .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
.take(2000)
: await ctx.db
.query("tickets")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(2000);
const machines = await ctx.db const machines = scopedCompanyId
.query("machines") ? await ctx.db
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .query("machines")
.take(1000); .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
.take(1000)
: await ctx.db
.query("machines")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(1000);
const users = await ctx.db const users = scopedCompanyId
.query("users") ? await ctx.db
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .query("users")
.take(500); .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", scopedCompanyId))
.take(500)
: await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(500);
const statusCounts = {} as Record<string, number>; const statusCounts = {} as Record<string, number>;
const priorityCounts = {} as Record<string, number>; const priorityCounts = {} as Record<string, number>;
@ -2534,11 +2551,13 @@ export const companyOverview = query({
}); });
return { return {
company: { company: company
id: company._id, ? {
name: company.name, id: company._id,
isAvulso: company.isAvulso ?? false, name: company.name,
}, isAvulso: company.isAvulso ?? false,
}
: null,
rangeDays, rangeDays,
generatedAt: now, generatedAt: now,
tickets: { tickets: {

View file

@ -1,12 +1,13 @@
"use client" "use client"
import { useEffect, useMemo, useState } from "react" import { useMemo, useState } from "react"
import Link from "next/link" import Link from "next/link"
import { useQuery } from "convex/react" import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { useAuth } from "@/lib/auth-client" import { useAuth } from "@/lib/auth-client"
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton" 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"
@ -59,7 +60,7 @@ const MACHINE_STATUS_CONFIG = {
export function CompanyReport() { export function CompanyReport() {
const { session, convexUserId, isStaff } = useAuth() const { session, convexUserId, isStaff } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const [selectedCompany, setSelectedCompany] = useState<string>("") const [selectedCompany, setSelectedCompany] = usePersistentCompanyFilter("all")
const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d") const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d")
const companies = useQuery( const companies = useQuery(
@ -67,28 +68,28 @@ export function CompanyReport() {
isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as CompanyRecord[] | undefined ) as CompanyRecord[] | undefined
const companyOptions = useMemo<SearchableComboboxOption[]>( const companyOptions = useMemo<SearchableComboboxOption[]>(() => {
() => const base: SearchableComboboxOption[] = [{ value: "all", label: "Todas as empresas" }]
(companies ?? []).map((company) => ({ 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 as string, value: company.id as string,
label: company.name, label: company.name,
})), })),
[companies] ]
) }, [companies])
useEffect(() => {
if (!selectedCompany && companyOptions.length > 0) {
setSelectedCompany(companyOptions[0]?.value ?? "")
}
}, [companyOptions, selectedCompany])
const report = useQuery( const report = useQuery(
api.reports.companyOverview, api.reports.companyOverview,
selectedCompany && convexUserId && isStaff convexUserId && isStaff
? { ? {
tenantId, tenantId,
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
companyId: selectedCompany as Id<"companies">, companyId: selectedCompany === "all" ? undefined : (selectedCompany as Id<"companies">),
range: timeRange, range: timeRange,
} }
: "skip" : "skip"