From 6ffd6c63925c0f3a6468926c868122bebf86cde4 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Thu, 9 Oct 2025 22:43:39 -0300 Subject: [PATCH] chore(types): remove explicit any, fix hook deps, and unused vars across admin/api/tickets; tighten zod server schemas; adjust UI types; fix pdf export expression; minor cleanup --- src/app/admin/companies/page.tsx | 14 +++++++- src/app/api/admin/companies/[id]/route.ts | 35 +++++++++++++------ src/app/api/admin/companies/route.ts | 16 +++++++-- .../api/admin/users/assign-company/route.ts | 8 ++--- .../api/reports/hours-by-client.csv/route.ts | 4 +-- src/app/api/tickets/[id]/export/pdf/route.ts | 8 +++-- src/components/admin/admin-users-manager.tsx | 8 ++--- .../companies/admin-companies-manager.tsx | 4 +-- .../machines/admin-machines-overview.tsx | 21 +++++------ src/components/charts/views-charts.tsx | 4 +-- src/components/reports/hours-report.tsx | 4 +-- .../tickets/ticket-comments.rich.tsx | 1 - .../tickets/ticket-summary-header.tsx | 8 +++-- src/components/tickets/tickets-table.tsx | 6 ++-- src/components/tickets/tickets-view.tsx | 6 ++-- src/lib/mappers/ticket.ts | 14 +++++--- src/lib/use-company-filter.ts | 2 -- 17 files changed, 104 insertions(+), 59 deletions(-) diff --git a/src/app/admin/companies/page.tsx b/src/app/admin/companies/page.tsx index 450c06f..77bf8ba 100644 --- a/src/app/admin/companies/page.tsx +++ b/src/app/admin/companies/page.tsx @@ -8,7 +8,19 @@ export const dynamic = "force-dynamic" export default async function AdminCompaniesPage() { const companiesRaw = await prisma.company.findMany({ orderBy: { name: "asc" } }) - const companies = companiesRaw.map((c: any) => ({ ...c, isAvulso: Boolean(c.isAvulso ?? false) })) + const companies = companiesRaw.map((c) => ({ + id: c.id, + tenantId: c.tenantId, + name: c.name, + slug: c.slug, + isAvulso: Boolean(c.isAvulso ?? false), + contractedHoursPerMonth: c.contractedHoursPerMonth ?? null, + cnpj: c.cnpj ?? null, + domain: c.domain ?? null, + phone: c.phone ?? null, + description: c.description ?? null, + address: c.address ?? null, + })) return ( - const updates: Record = {} - for (const key of ["name", "slug", "cnpj", "domain", "phone", "description", "address"]) { - if (key in body) updates[key] = body[key] ?? null - } - if ("isAvulso" in body) updates.isAvulso = Boolean(body.isAvulso) - if ("contractedHoursPerMonth" in body) { - const raw = body.contractedHoursPerMonth - updates.contractedHoursPerMonth = typeof raw === "number" ? raw : raw ? Number(raw) : null + const updates: Prisma.CompanyUpdateInput = {} + if (typeof raw.name === "string" && raw.name.trim()) updates.name = raw.name.trim() + if (typeof raw.slug === "string" && raw.slug.trim()) updates.slug = raw.slug.trim() + if ("cnpj" in raw) updates.cnpj = raw.cnpj ?? null + if ("domain" in raw) updates.domain = raw.domain ?? null + if ("phone" in raw) updates.phone = raw.phone ?? null + if ("description" in raw) updates.description = raw.description ?? null + if ("address" in raw) updates.address = raw.address ?? null + if ("isAvulso" in raw) updates.isAvulso = Boolean(raw.isAvulso) + if ("contractedHoursPerMonth" in raw) { + const v = raw.contractedHoursPerMonth + updates.contractedHoursPerMonth = typeof v === "number" ? v : v ? Number(v) : null } try { - const company = await prisma.company.update({ where: { id }, data: updates as any }) + const company = await prisma.company.update({ where: { id }, data: updates }) return NextResponse.json({ company }) } catch (error) { console.error("Failed to update company", error) diff --git a/src/app/api/admin/companies/route.ts b/src/app/api/admin/companies/route.ts index b400d65..ca6ed74 100644 --- a/src/app/api/admin/companies/route.ts +++ b/src/app/api/admin/companies/route.ts @@ -19,7 +19,17 @@ export async function POST(request: Request) { const session = await assertAdminSession() if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) - const body = await request.json() + const body = (await request.json()) as Partial<{ + name: string + slug: string + isAvulso: boolean + cnpj: string | null + domain: string | null + phone: string | null + description: string | null + address: string | null + contractedHoursPerMonth: number | string | null + }> const { name, slug, isAvulso, cnpj, domain, phone, description, address, contractedHoursPerMonth } = body ?? {} if (!name || !slug) { return NextResponse.json({ error: "Nome e slug são obrigatórios" }, { status: 400 }) @@ -27,7 +37,7 @@ export async function POST(request: Request) { try { const company = await prisma.company.create({ - data: ({ + data: { tenantId: session.user.tenantId ?? "tenant-atlas", name: String(name), slug: String(slug), @@ -38,7 +48,7 @@ export async function POST(request: Request) { phone: phone ? String(phone) : null, description: description ? String(description) : null, address: address ? String(address) : null, - } as any), + }, }) return NextResponse.json({ company }) } catch (error) { diff --git a/src/app/api/admin/users/assign-company/route.ts b/src/app/api/admin/users/assign-company/route.ts index a5761b9..ac3954c 100644 --- a/src/app/api/admin/users/assign-company/route.ts +++ b/src/app/api/admin/users/assign-company/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server" import { ConvexHttpClient } from "convex/browser" import { assertAdminSession } from "@/lib/auth-server" +import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" export const runtime = "nodejs" @@ -10,7 +11,7 @@ export async function POST(request: Request) { const session = await assertAdminSession() if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) - const body = await request.json().catch(() => null) as { email?: string; companyId?: string } + const body = (await request.json().catch(() => null)) as { email?: string; companyId?: string } const email = body?.email?.trim().toLowerCase() const companyId = body?.companyId if (!email || !companyId) { @@ -25,8 +26,8 @@ export async function POST(request: Request) { await client.mutation(api.users.assignCompany, { tenantId: session.user.tenantId ?? "tenant-atlas", email, - companyId: companyId as any, - actorId: (session.user as any).convexUserId ?? (session.user.id as any), + companyId: companyId as Id<"companies">, + actorId: (session.user as unknown as { convexUserId?: Id<"users">; id?: Id<"users"> }).convexUserId ?? (session.user.id as unknown as Id<"users">), }) return NextResponse.json({ ok: true }) } catch (error) { @@ -34,4 +35,3 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Falha ao vincular usuário" }, { status: 500 }) } } - diff --git a/src/app/api/reports/hours-by-client.csv/route.ts b/src/app/api/reports/hours-by-client.csv/route.ts index c14194b..8883b6b 100644 --- a/src/app/api/reports/hours-by-client.csv/route.ts +++ b/src/app/api/reports/hours-by-client.csv/route.ts @@ -37,7 +37,7 @@ export async function GET(request: Request) { role: session.user.role.toUpperCase(), }) viewerId = ensuredUser?._id ?? null - } catch (error) { + } catch (_error) { return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 }) } if (!viewerId) return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 }) @@ -75,7 +75,7 @@ export async function GET(request: Request) { "Cache-Control": "no-store", }, }) - } catch (error) { + } catch (_error) { return NextResponse.json({ error: "Falha ao gerar CSV de horas por cliente" }, { status: 500 }) } } diff --git a/src/app/api/tickets/[id]/export/pdf/route.ts b/src/app/api/tickets/[id]/export/pdf/route.ts index 9b60e46..9ea3b1c 100644 --- a/src/app/api/tickets/[id]/export/pdf/route.ts +++ b/src/app/api/tickets/[id]/export/pdf/route.ts @@ -305,7 +305,11 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st const badgeTextWidth = doc.widthOfString(statusText) const badgeHeight = badgeFontSize + badgePaddingY * 2 const badgeWidth = badgeTextWidth + badgePaddingX * 2 - D.roundedRect?.(badgeX, badgeY, badgeWidth, badgeHeight, 4) ?? doc.rect(badgeX, badgeY, badgeWidth, badgeHeight) + if (typeof D.roundedRect === "function") { + D.roundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 4) + } else { + doc.rect(badgeX, badgeY, badgeWidth, badgeHeight) + } doc.fill(badgeColor) doc.fillColor("#FFFFFF").text(statusText, badgeX + badgePaddingX, badgeY + badgePaddingY) doc.restore() @@ -316,7 +320,7 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st const colGap = 24 const colWidth = (doc.page.width - doc.page.margins.left - doc.page.margins.right - colGap) / 2 const rightX = leftX + colWidth + colGap - const startY = doc.y + // const startY = doc.y const drawMeta = (x: number, lines: string[]) => { doc.save() doc.x = x diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index 2e03d45..883989f 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -1,6 +1,6 @@ "use client" -import { useMemo, useState, useTransition } from "react" +import { useEffect, useMemo, useState, useTransition } from "react" import { toast } from "sonner" @@ -102,12 +102,12 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d const normalizedRoles = useMemo(() => roleOptions ?? ROLE_OPTIONS, [roleOptions]) // load companies for association - useMemo(() => { + useEffect(() => { void (async () => { try { const r = await fetch("/api/admin/companies", { credentials: "include" }) - const j = await r.json() - const items = (j.companies ?? []).map((c: any) => ({ id: c.id as string, name: c.name as string })) + const j = (await r.json()) as { companies?: Array<{ id: string; name: string }> } + const items = (j.companies ?? []).map((c) => ({ id: c.id, name: c.name })) setCompanies(items) } catch { // noop diff --git a/src/components/admin/companies/admin-companies-manager.tsx b/src/components/admin/companies/admin-companies-manager.tsx index e779e7f..4fa3125 100644 --- a/src/components/admin/companies/admin-companies-manager.tsx +++ b/src/components/admin/companies/admin-companies-manager.tsx @@ -1,6 +1,6 @@ "use client" -import { useMemo, useState, useTransition } from "react" +import { useEffect, useMemo, useState, useTransition } from "react" import { toast } from "sonner" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -64,7 +64,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: } } - useMemo(() => { void loadLastAlerts(companies) }, []) + useEffect(() => { void loadLastAlerts(companies) }, [companies]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index d39bd99..1797418 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -84,6 +84,7 @@ type MachineInventory = { // Dados enviados pelo agente desktop (inventário básico/estendido) disks?: Array<{ name?: string; mountPoint?: string; fs?: string; totalBytes?: number; availableBytes?: number }> extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended } + services?: Array<{ name?: string; status?: string; displayName?: string }> } type MachinesQueryItem = { @@ -352,8 +353,8 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { {Array.isArray(machine.inventory?.disks) ? ( {machine.inventory?.disks?.length ?? 0} discos ) : null} - {Array.isArray((machine.inventory as any)?.services) ? ( - {(machine.inventory as any).services.length} serviços + {Array.isArray(machine.inventory?.services) ? ( + {machine.inventory?.services?.length ?? 0} serviços ) : null} @@ -617,7 +618,7 @@ function MachineDetails({ machine }: MachineDetailsProps) { - {(network as any[]).map((iface, idx) => ( + {(network as Array<{ name?: string; mac?: string; ip?: string }>).map((iface, idx) => ( {iface?.name ?? "—"} {iface?.mac ?? "—"} @@ -711,7 +712,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {

SMART

- {linuxExt.smart.map((s: any, idx: number) => { + {linuxExt.smart.map((s: { smart_status?: { passed?: boolean }; model_name?: string; model_family?: string; serial_number?: string; device?: { name?: string } }, idx: number) => { const ok = s?.smart_status?.passed !== false const model = s?.model_name ?? s?.model_family ?? "Disco" const serial = s?.serial_number ?? s?.device?.name ?? "—" @@ -758,7 +759,7 @@ function MachineDetails({ machine }: MachineDetailsProps) { - {(windowsExt.services as any[]).slice(0, 10).map((svc: any, i: number) => ( + {(windowsExt.services as Array<{ Name?: string; DisplayName?: string; Status?: string }>).slice(0, 10).map((svc, i: number) => ( {svc?.Name ?? "—"} {svc?.DisplayName ?? "—"} @@ -775,7 +776,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {

Softwares (amostra)

    - {(windowsExt.software as any[]).slice(0, 8).map((s: any, i: number) => ( + {(windowsExt.software as Array<{ DisplayName?: string; name?: string; DisplayVersion?: string; Publisher?: string }>).slice(0, 8).map((s, i: number) => (
  • {s?.DisplayName ?? s?.name ?? "—"} {s?.DisplayVersion ? {s.DisplayVersion} : null} @@ -823,7 +824,7 @@ function MachineDetails({ machine }: MachineDetailsProps) {

    Alertas de postura

    - {machine?.postureAlerts?.map((a: any, i: number) => ( + {machine?.postureAlerts?.map((a: { kind?: string; message?: string; severity?: string }, i: number) => (
    @@ -844,8 +845,8 @@ function MachineDetails({ machine }: MachineDetailsProps) { {Array.isArray(software) && software.length > 0 ? ( ) : null} - {Array.isArray((metadata as any)?.services) && (metadata as any).services.length > 0 ? ( - + {Array.isArray(metadata?.services) && metadata.services.length > 0 ? ( + ) : null}
    {fleet ? ( @@ -979,7 +980,7 @@ function exportCsv(items: Array>, filename: string) { for (const it of items) { const row = headers .map((h) => { - const v = (it as any)?.[h] + const v = (it as Record)[h] if (v === undefined || v === null) return "" const s = typeof v === "string" ? v : JSON.stringify(v) return `"${s.replace(/"/g, '""')}"` diff --git a/src/components/charts/views-charts.tsx b/src/components/charts/views-charts.tsx index ce426e7..c0a658a 100644 --- a/src/components/charts/views-charts.tsx +++ b/src/components/charts/views-charts.tsx @@ -48,7 +48,7 @@ function BacklogPriorityPie() { const chartData = keys .map((k) => ({ name: PRIORITY_LABELS[k] ?? k, value: data.priorityCounts?.[k] ?? 0, fill: fills[k as keyof typeof fills] })) .filter((d) => d.value > 0) - const chartConfig: any = { value: { label: "Tickets" } } + const chartConfig: Record = { value: { label: "Tickets" } } return ( @@ -118,7 +118,7 @@ function QueuesOpenBar() { if (!data) return const chartData = (data.queueBreakdown ?? []).map((q) => ({ queue: q.name, open: q.open })) - const chartConfig: any = { open: { label: "Abertos" } } + const chartConfig: Record = { open: { label: "Abertos" } } return ( diff --git a/src/components/reports/hours-report.tsx b/src/components/reports/hours-report.tsx index ceb4805..adf954d 100644 --- a/src/components/reports/hours-report.tsx +++ b/src/components/reports/hours-report.tsx @@ -42,15 +42,15 @@ export function HoursReport() { convexUserId ? { tenantId, viewerId: convexUserId as Id<"users">, range: timeRange } : "skip" ) as { rangeDays: number; items: HoursItem[] } | undefined - const items = data?.items ?? [] const companies = useQuery(api.companies.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip") as Array<{ id: Id<"companies">; name: string }> | undefined const filtered = useMemo(() => { + const items = data?.items ?? [] const q = query.trim().toLowerCase() let list = items if (companyId !== "all") list = list.filter((it) => String(it.companyId) === companyId) if (q) list = list.filter((it) => it.name.toLowerCase().includes(q)) return list - }, [items, query, companyId]) + }, [data?.items, query, companyId]) return (
    diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 9d67834..352977a 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -35,7 +35,6 @@ const submitButtonClass = export function TicketComments({ ticket }: TicketCommentsProps) { const { convexUserId, isStaff, role } = useAuth() const normalizedRole = role ?? null - const isManager = normalizedRole === "manager" const canSeeInternalComments = normalizedRole === "admin" || normalizedRole === "agent" const addComment = useMutation(api.tickets.addComment) const removeAttachment = useMutation(api.tickets.removeCommentAttachment) diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index 8a8f3a1..aba87e2 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -155,7 +155,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId }, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId]) const currentQueueName = ticket.queue ?? "" - const isAvulso = Boolean((ticket as any).company?.isAvulso ?? false) + const isAvulso = Boolean(((ticket.company ?? null) as { isAvulso?: boolean } | null)?.isAvulso ?? false) const [queueSelection, setQueueSelection] = useState(currentQueueName) const queueDirty = useMemo(() => queueSelection !== currentQueueName, [queueSelection, currentQueueName]) const formDirty = dirty || categoryDirty || queueDirty @@ -307,9 +307,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { } }, [ ticket.id, + ticket.workSummary, ticket.workSummary?.totalWorkedMs, ticket.workSummary?.internalWorkedMs, ticket.workSummary?.externalWorkedMs, + ticketActiveSession, ticketActiveSession?.id, ticketActiveSessionStartedAtMs, ticketActiveSessionWorkType, @@ -352,7 +354,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { setNow(Date.now()) }, 1000) return () => clearInterval(interval) - }, [workSummary?.activeSession?.id]) + }, [workSummary?.activeSession, workSummary?.activeSession?.id]) useEffect(() => { if (!pauseDialogOpen) { @@ -382,7 +384,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { toast.dismiss("work") toast.loading("Iniciando atendimento...", { id: "work" }) try { - const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType } as any) + const result = await startWork({ ticketId: ticket.id as Id<"tickets">, actorId: convexUserId as Id<"users">, workType }) if (result?.status === "already_started") { toast.info("O atendimento já estava em andamento", { id: "work" }) } else { diff --git a/src/components/tickets/tickets-table.tsx b/src/components/tickets/tickets-table.tsx index bf7edfd..2091b44 100644 --- a/src/components/tickets/tickets-table.tsx +++ b/src/components/tickets/tickets-table.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { useRouter } from "next/navigation" -import { format, formatDistanceToNow, formatDistanceToNowStrict } from "date-fns" +import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react" @@ -235,8 +235,8 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) {