From c640e288b1de4633daaef8c811c8aa2440645123 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Wed, 22 Oct 2025 19:19:38 -0300 Subject: [PATCH] chore(types): remove anys and harden Convex data fetch - Strongly type company-service and API routes - Fix Next.js searchParams (promise) in admin/machines page - Add vitest module marker + stub for tsconfig-paths/register - Use Convex query in client as primary fallback for machine details - Replace any casts in admin machines components Build + lint are clean locally; details page no longer skeleton-loops. --- src/app/admin/machines/page.tsx | 8 +- src/app/api/admin/companies/[id]/route.ts | 4 +- src/app/api/admin/companies/route.ts | 6 +- .../machines/admin-machine-details.client.tsx | 112 +++++++----------- .../machines/machine-breadcrumbs.client.tsx | 14 +-- src/server/company-service.ts | 19 +-- types/tsconfig-paths-register.d.ts | 1 + vitest.setup.node.ts | 2 + 8 files changed, 76 insertions(+), 90 deletions(-) create mode 100644 types/tsconfig-paths-register.d.ts diff --git a/src/app/admin/machines/page.tsx b/src/app/admin/machines/page.tsx index 365c04a..c95a594 100644 --- a/src/app/admin/machines/page.tsx +++ b/src/app/admin/machines/page.tsx @@ -6,8 +6,12 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants" export const runtime = "nodejs" export const dynamic = "force-dynamic" -export default function AdminMachinesPage({ searchParams }: { searchParams?: { [key: string]: string | string[] | undefined } }) { - const company = typeof searchParams?.company === 'string' ? searchParams?.company : undefined +export default async function AdminMachinesPage({ + searchParams, +}: { searchParams: Promise> }) { + const params = await searchParams + const companyParam = params.company + const company = typeof companyParam === "string" ? companyParam : undefined return ( , includeMetadata: true } as const) : ("skip" as const) - ) as - | Record - | null - | undefined + const queryArgs = machineId + ? ({ id: machineId as Id<"machines">, includeMetadata: true } as const) + : "skip" + + const single = useQuery(api.machines.getById, queryArgs) // Fallback via HTTP in caso de o Convex React demorar/ficar preso em loading const [fallback, setFallback] = useState | null>(null) @@ -35,23 +34,46 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s if (!shouldLoad) return timer.current = setTimeout(async () => { try { - const res = await fetch(`/api/admin/machines/${machineId}/details`, { credentials: "include" }) - if (res.ok) { - const data = (await res.json()) as Record - setFallback(data) - setLoadError(null) - } else { - // Not found (404) ou erro de servidor - let message = `Falha ao carregar (HTTP ${res.status})` + // 1) Tenta via Convex direto do browser (independe do servidor) + const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL + if (convexUrl) { try { - const payload = (await res.json()) as Record - if (typeof payload?.error === "string" && payload.error) { - message = payload.error + const http = new ConvexHttpClient(convexUrl) + const data = (await http.query(api.machines.getById, { + id: machineId as Id<"machines">, + includeMetadata: true, + })) as Record | null + if (data) { + setFallback(data) + setLoadError(null) + return } } catch { - // ignore parse error + // continua para o plano B } - setLoadError(message) + } + + // 2) Plano B: rota do servidor (útil em ambientes sem Convex público) + try { + const res = await fetch(`/api/admin/machines/${machineId}/details`, { credentials: "include" }) + if (res.ok) { + const data = (await res.json()) as Record + setFallback(data) + setLoadError(null) + } else { + let message = `Falha ao carregar (HTTP ${res.status})` + try { + const payload = (await res.json()) as Record + if (typeof payload?.error === "string" && payload.error) { + message = payload.error + } + } catch { + // ignore + } + setLoadError(message) + } + } catch { + setLoadError("Erro de rede ao carregar os dados da máquina.") } } catch (err) { setLoadError("Erro de rede ao carregar os dados da máquina.") @@ -60,7 +82,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s return () => { if (timer.current) clearTimeout(timer.current) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldLoad, machineId, retryTick]) // Timeout de proteção: se depois de X segundos ainda estiver carregando e sem fallback, mostra erro claro @@ -72,7 +93,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s ) }, 10_000) return () => clearTimeout(timeout) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldLoad, machineId, retryTick]) const machine: MachinesQueryItem | null = useMemo(() => { @@ -80,7 +100,7 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s if (source === undefined || source === null) return source as null return normalizeMachineItem(source) }, [single, fallback]) - const isLoading = single === undefined && !fallback + const isLoading = single === undefined && !fallback && !loadError const isNotFound = single === null && !fallback const onRetry = () => { @@ -94,48 +114,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s // ignore } } - - if (isLoading) { - return ( - - - - - - - - ) - } - - if (isNotFound) { - return ( - - -

Máquina não encontrada

-

- Verifique o identificador e tente novamente. -

-
- -
-
-
- ) - } - - if (loadError && !machine) { - return ( - - -

Falha ao carregar os dados da máquina

-

{loadError}

-
- -
-
-
- ) - } - return } + diff --git a/src/components/admin/machines/machine-breadcrumbs.client.tsx b/src/components/admin/machines/machine-breadcrumbs.client.tsx index 1f19f4c..58cc13e 100644 --- a/src/components/admin/machines/machine-breadcrumbs.client.tsx +++ b/src/components/admin/machines/machine-breadcrumbs.client.tsx @@ -7,15 +7,13 @@ import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" -export function MachineBreadcrumbs({ tenantId, machineId }: { tenantId: string; machineId: string }) { +export function MachineBreadcrumbs({ tenantId: _tenantId, machineId }: { tenantId: string; machineId: string }) { const { convexUserId } = useAuth() - const item = useQuery( - (api as any).machines.getById, - machineId ? ({ id: machineId as Id<"machines">, includeMetadata: false } as const) : ("skip" as const) - ) as - | { hostname: string } - | null - | undefined + const queryArgs = machineId && convexUserId + ? ({ id: machineId as Id<"machines">, includeMetadata: false } as const) + : "skip" + + const item = useQuery(api.machines.getById, queryArgs) const hostname = useMemo(() => item?.hostname ?? "Detalhe", [item]) return ( diff --git a/src/server/company-service.ts b/src/server/company-service.ts index 0d1980f..4ddab88 100644 --- a/src/server/company-service.ts +++ b/src/server/company-service.ts @@ -17,6 +17,8 @@ export type NormalizedCompany = CompanyFormValues & { updatedAt: string } +type CompanyCreatePayload = Omit + // Local representation of the DB enum to avoid relying on Prisma enum exports type DbCompanyStateRegistrationType = "STANDARD" | "EXEMPT" | "SIMPLES" @@ -210,10 +212,7 @@ export function sanitizeCompanyInput(input: unknown, tenantId: string): CompanyF return normalized } -export function buildCompanyData( - payload: CompanyFormValues, - tenantId: string -): Record { +export function buildCompanyData(payload: CompanyFormValues, tenantId: string): CompanyCreatePayload { const stateRegistrationType = payload.stateRegistrationType ? STATE_REGISTRATION_TYPE_TO_PRISMA[payload.stateRegistrationType as CompanyStateRegistrationTypeOption] : null @@ -221,7 +220,7 @@ export function buildCompanyData( const communicationChannels = mergeChannelsWithPrimary(payload) const privacyPolicyMetadata = payload.privacyPolicy?.metadata ?? null - return { + const data: CompanyCreatePayload = { tenantId, name: payload.name.trim(), slug: payload.slug.trim(), @@ -267,9 +266,11 @@ export function buildCompanyData( customFields: payload.customFields, notes: payload.notes ?? null, } + + return data } -export function normalizeCompany(company: any): NormalizedCompany { +export function normalizeCompany(company: Company): NormalizedCompany { const communicationChannels = normalizeChannels( company.communicationChannels as CompanyCommunicationChannels | null | undefined ) @@ -387,7 +388,7 @@ function parseJsonValue(value: string | null): Prisma.JsonValue | null { } } -function mapRawRowToCompany(row: RawCompanyRow): any { +function mapRawRowToCompany(row: RawCompanyRow): Company { return { id: row.id, tenantId: row.tenantId, @@ -477,7 +478,7 @@ const COMPANY_BASE_SELECT = Prisma.sql` FROM "Company" ` -export async function fetchCompaniesByTenant(tenantId: string): Promise { +export async function fetchCompaniesByTenant(tenantId: string): Promise { const rows = await prisma.$queryRaw(Prisma.sql` ${COMPANY_BASE_SELECT} WHERE tenantId = ${tenantId} @@ -486,7 +487,7 @@ export async function fetchCompaniesByTenant(tenantId: string): Promise { return rows.map(mapRawRowToCompany) } -export async function fetchCompanyById(id: string): Promise { +export async function fetchCompanyById(id: string): Promise { const rows = await prisma.$queryRaw(Prisma.sql` ${COMPANY_BASE_SELECT} WHERE id = ${id} diff --git a/types/tsconfig-paths-register.d.ts b/types/tsconfig-paths-register.d.ts new file mode 100644 index 0000000..b5dea45 --- /dev/null +++ b/types/tsconfig-paths-register.d.ts @@ -0,0 +1 @@ +declare module "tsconfig-paths/register" diff --git a/vitest.setup.node.ts b/vitest.setup.node.ts index ad7fd39..5459c1f 100644 --- a/vitest.setup.node.ts +++ b/vitest.setup.node.ts @@ -6,3 +6,5 @@ if (typeof process !== "undefined" && process.versions?.node) { process.env.NEXT_PUBLIC_APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000" process.env.BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL } + +export {}