Fix report filters and zod resolver

This commit is contained in:
Esdras Renan 2025-11-14 01:25:28 -03:00
parent 5b22065609
commit 06fdb54480
12 changed files with 79 additions and 26 deletions

View file

@ -1422,6 +1422,8 @@ export async function ticketsByMachineAndCategoryHandler(
companyId, companyId,
machineId, machineId,
userId, userId,
dateFrom,
dateTo,
}: { }: {
tenantId: string tenantId: string
viewerId: Id<"users"> viewerId: Id<"users">
@ -1429,10 +1431,12 @@ export async function ticketsByMachineAndCategoryHandler(
companyId?: Id<"companies"> companyId?: Id<"companies">
machineId?: Id<"machines"> machineId?: Id<"machines">
userId?: Id<"users"> userId?: Id<"users">
dateFrom?: string
dateTo?: string
} }
) { ) {
const viewer = await requireStaff(ctx, viewerId, tenantId) const viewer = await requireStaff(ctx, viewerId, tenantId)
const { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 90) const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
const tickets = const tickets =
days === 0 days === 0
@ -1584,6 +1588,8 @@ export async function hoursByMachineHandler(
companyId, companyId,
machineId, machineId,
userId, userId,
dateFrom,
dateTo,
}: { }: {
tenantId: string tenantId: string
viewerId: Id<"users"> viewerId: Id<"users">
@ -1591,12 +1597,14 @@ export async function hoursByMachineHandler(
companyId?: Id<"companies"> companyId?: Id<"companies">
machineId?: Id<"machines"> machineId?: Id<"machines">
userId?: Id<"users"> userId?: Id<"users">
dateFrom?: string
dateTo?: string
} }
) { ) {
const viewer = await requireStaff(ctx, viewerId, tenantId) const viewer = await requireStaff(ctx, viewerId, tenantId)
const tickets = await fetchScopedTickets(ctx, tenantId, viewer) const tickets = await fetchScopedTickets(ctx, tenantId, viewer)
const { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 90) const { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
const machinesById = new Map<string, Doc<"machines"> | null>() const machinesById = new Map<string, Doc<"machines"> | null>()
const companiesById = new Map<string, Doc<"companies"> | null>() const companiesById = new Map<string, Doc<"companies"> | null>()

View file

@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react" import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { Controller, FormProvider, useFieldArray, useForm, type UseFormReturn } from "react-hook-form" import { Controller, FormProvider, useFieldArray, useForm, type UseFormReturn } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@/lib/zod-resolver"
import { import {
IconAlertTriangle, IconAlertTriangle,
IconBuildingSkyscraper, IconBuildingSkyscraper,

View file

@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from
import { format } from "date-fns" import { format } from "date-fns"
import { ptBR } from "date-fns/locale" import { ptBR } from "date-fns/locale"
import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form" import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@/lib/zod-resolver"
import { import {
IconBuildingSkyscraper, IconBuildingSkyscraper,
IconChevronRight, IconChevronRight,

View file

@ -76,7 +76,7 @@ import { useSidebar } from "@/components/ui/sidebar"
import { toast } from "sonner" import { toast } from "sonner"
import { z } from "zod" import { z } from "zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@/lib/zod-resolver"
import { import {
Check, Check,
Copy, Copy,

View file

@ -52,6 +52,14 @@ export function BacklogReport() {
const { session, convexUserId, isStaff, isAdmin } = useAuth() const { session, convexUserId, isStaff, isAdmin } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const enabled = Boolean(isStaff && convexUserId) const enabled = Boolean(isStaff && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const data = useQuery( const data = useQuery(
api.reports.backlogOverview, api.reports.backlogOverview,
enabled enabled
@ -60,8 +68,7 @@ export function BacklogReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
} }
: "skip" : "skip"
) )

View file

@ -60,6 +60,13 @@ export function CategoryReport() {
const [dateTo, setDateTo] = useState<string | null>(null) const [dateTo, setDateTo] = useState<string | null>(null)
const enabled = Boolean(isStaff && convexUserId) const enabled = Boolean(isStaff && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const companyFilter = companyId !== "all" ? (companyId as Id<"companies">) : undefined const companyFilter = companyId !== "all" ? (companyId as Id<"companies">) : undefined
const data = useQuery( const data = useQuery(
@ -70,10 +77,9 @@ export function CategoryReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyFilter, companyId: companyFilter,
dateFrom, ...dateRangeFilters,
dateTo,
} }
: "skip", : "skip"
) as CategoryInsightsResponse | undefined ) as CategoryInsightsResponse | undefined
const companies = useQuery( const companies = useQuery(

View file

@ -9,7 +9,7 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants"
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 { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { useState } from "react" import { useMemo, useState } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox" import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
@ -31,6 +31,12 @@ export function CsatReport() {
const { session, convexUserId, isStaff, isAdmin } = useAuth() const { session, convexUserId, isStaff, isAdmin } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const enabled = Boolean(isStaff && convexUserId) const enabled = Boolean(isStaff && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const data = useQuery( const data = useQuery(
api.reports.csatOverview, api.reports.csatOverview,
enabled enabled
@ -39,8 +45,7 @@ export function CsatReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
}) })
: "skip" : "skip"
) )

View file

@ -79,6 +79,12 @@ export function HoursReport() {
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const enabled = Boolean(isStaff && convexUserId) const enabled = Boolean(isStaff && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const data = useQuery( const data = useQuery(
api.reports.hoursByClient, api.reports.hoursByClient,
enabled && groupBy === "company" enabled && groupBy === "company"
@ -86,8 +92,7 @@ export function HoursReport() {
tenantId, tenantId,
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange === "365d" || timeRange === "all" ? "90d" : timeRange, range: timeRange === "365d" || timeRange === "all" ? "90d" : timeRange,
dateFrom, ...dateRangeFilters,
dateTo,
} }
: "skip" : "skip"
) as { rangeDays: number; items: HoursItem[] } | undefined ) as { rangeDays: number; items: HoursItem[] } | undefined
@ -100,8 +105,7 @@ export function HoursReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
} }
: "skip" : "skip"
) as HoursByMachineResponse | undefined ) as HoursByMachineResponse | undefined

View file

@ -57,6 +57,12 @@ export function MachineCategoryReport() {
const canView = Boolean(isStaff) const canView = Boolean(isStaff)
const enabled = Boolean(canView && convexUserId) const enabled = Boolean(canView && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const data = useQuery( const data = useQuery(
api.reports.ticketsByMachineAndCategory, api.reports.ticketsByMachineAndCategory,
@ -66,8 +72,7 @@ export function MachineCategoryReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
} as const) } as const)
: "skip" : "skip"
) as MachineCategoryReportData | undefined ) as MachineCategoryReportData | undefined
@ -163,8 +168,7 @@ export function MachineCategoryReport() {
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
machineId: selectedMachineId !== "all" ? (selectedMachineId as Id<"machines">) : undefined, machineId: selectedMachineId !== "all" ? (selectedMachineId as Id<"machines">) : undefined,
userId: selectedUserId !== "all" ? (selectedUserId as Id<"users">) : undefined, userId: selectedUserId !== "all" ? (selectedUserId as Id<"users">) : undefined,
dateFrom, ...dateRangeFilters,
dateTo,
} as const) } as const)
: "skip" : "skip"
) as MachineHoursResponse | undefined ) as MachineHoursResponse | undefined

View file

@ -68,6 +68,12 @@ export function SlaReport() {
const { session, convexUserId, isStaff, isAdmin } = useAuth() const { session, convexUserId, isStaff, isAdmin } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const enabled = Boolean(isStaff && convexUserId) const enabled = Boolean(isStaff && convexUserId)
const dateRangeFilters = useMemo(() => {
const filters: { dateFrom?: string; dateTo?: string } = {}
if (dateFrom) filters.dateFrom = dateFrom
if (dateTo) filters.dateTo = dateTo
return filters
}, [dateFrom, dateTo])
const data = useQuery( const data = useQuery(
api.reports.slaOverview, api.reports.slaOverview,
enabled enabled
@ -76,8 +82,7 @@ export function SlaReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
}) })
: "skip" : "skip"
) )
@ -96,8 +101,7 @@ export function SlaReport() {
viewerId: convexUserId as Id<"users">, viewerId: convexUserId as Id<"users">,
range: timeRange, range: timeRange,
companyId: companyId === "all" ? undefined : (companyId as Id<"companies">), companyId: companyId === "all" ? undefined : (companyId as Id<"companies">),
dateFrom, ...dateRangeFilters,
dateTo,
}) })
: "skip" : "skip"
) as { rangeDays: number; series: Array<{ date: string; opened: number; resolved: number }> } | undefined ) as { rangeDays: number; series: Array<{ date: string; opened: number; resolved: number }> } | undefined

View file

@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { FieldSet, FieldGroup, Field, FieldLabel, FieldError } from "@/components/ui/field" import { FieldSet, FieldGroup, Field, FieldLabel, FieldError } from "@/components/ui/field"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@/lib/zod-resolver"
import { toast } from "sonner" import { toast } from "sonner"
import { Spinner } from "@/components/ui/spinner" import { Spinner } from "@/components/ui/spinner"
import { Dropzone } from "@/components/ui/dropzone" import { Dropzone } from "@/components/ui/dropzone"

15
src/lib/zod-resolver.ts Normal file
View file

@ -0,0 +1,15 @@
import { zodResolver as baseZodResolver } from "@hookform/resolvers/zod"
import { ZodError } from "zod"
// Zod v4 renamed `error.errors` to `error.issues`, but @hookform/resolvers
// still expects the legacy `errors` getter. Patch the prototype once so every
// resolver consumer keeps working without duplicating validation logic.
if (typeof ZodError !== "undefined" && !("errors" in ZodError.prototype)) {
Object.defineProperty(ZodError.prototype, "errors", {
get() {
return this.issues
},
})
}
export const zodResolver = baseZodResolver