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:
parent
14480df9f3
commit
8a237a820d
2 changed files with 59 additions and 39 deletions
|
|
@ -2406,19 +2406,21 @@ 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)
|
||||||
|
let company: Doc<"companies"> | null = null;
|
||||||
|
if (scopedCompanyId) {
|
||||||
|
company = await ctx.db.get(scopedCompanyId);
|
||||||
if (!company || company.tenantId !== tenantId) {
|
if (!company || company.tenantId !== tenantId) {
|
||||||
throw new ConvexError("Empresa não encontrada");
|
throw new ConvexError("Empresa não encontrada");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRange = (range ?? "30d").toLowerCase();
|
const normalizedRange = (range ?? "30d").toLowerCase();
|
||||||
const rangeDays = normalizedRange === "90d" ? 90 : normalizedRange === "7d" ? 7 : 30;
|
const rangeDays = normalizedRange === "90d" ? 90 : normalizedRange === "7d" ? 7 : 30;
|
||||||
|
|
@ -2426,19 +2428,34 @@ 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
|
||||||
|
? await ctx.db
|
||||||
.query("tickets")
|
.query("tickets")
|
||||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
.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);
|
.take(2000);
|
||||||
|
|
||||||
const machines = await ctx.db
|
const machines = scopedCompanyId
|
||||||
|
? await ctx.db
|
||||||
.query("machines")
|
.query("machines")
|
||||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
.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);
|
.take(1000);
|
||||||
|
|
||||||
const users = await ctx.db
|
const users = scopedCompanyId
|
||||||
|
? await ctx.db
|
||||||
.query("users")
|
.query("users")
|
||||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
.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);
|
.take(500);
|
||||||
|
|
||||||
const statusCounts = {} as Record<string, number>;
|
const statusCounts = {} as Record<string, number>;
|
||||||
|
|
@ -2534,11 +2551,13 @@ export const companyOverview = query({
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
company: {
|
company: company
|
||||||
|
? {
|
||||||
id: company._id,
|
id: company._id,
|
||||||
name: company.name,
|
name: company.name,
|
||||||
isAvulso: company.isAvulso ?? false,
|
isAvulso: company.isAvulso ?? false,
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
rangeDays,
|
rangeDays,
|
||||||
generatedAt: now,
|
generatedAt: now,
|
||||||
tickets: {
|
tickets: {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue