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.
This commit is contained in:
Esdras Renan 2025-10-22 19:19:38 -03:00
parent eee0f432e7
commit c640e288b1
8 changed files with 76 additions and 90 deletions

View file

@ -6,8 +6,12 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants"
export const runtime = "nodejs" export const runtime = "nodejs"
export const dynamic = "force-dynamic" export const dynamic = "force-dynamic"
export default function AdminMachinesPage({ searchParams }: { searchParams?: { [key: string]: string | string[] | undefined } }) { export default async function AdminMachinesPage({
const company = typeof searchParams?.company === 'string' ? searchParams?.company : undefined searchParams,
}: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const params = await searchParams
const companyParam = params.company
const company = typeof companyParam === "string" ? companyParam : undefined
return ( return (
<AppShell <AppShell
header={ header={

View file

@ -87,13 +87,13 @@ export async function PATCH(
const mergedInput = mergePayload(baseForm, rawBody) const mergedInput = mergePayload(baseForm, rawBody)
const form = sanitizeCompanyInput(mergedInput, existing.tenantId) const form = sanitizeCompanyInput(mergedInput, existing.tenantId)
const createData = buildCompanyData(form, existing.tenantId) as any const createData = buildCompanyData(form, existing.tenantId)
const { tenantId: _omitTenant, ...updateData } = createData const { tenantId: _omitTenant, ...updateData } = createData
void _omitTenant void _omitTenant
const company = await prisma.company.update({ const company = await prisma.company.update({
where: { id }, where: { id },
data: updateData as any, data: updateData,
}) })
if (company.provisioningCode) { if (company.provisioningCode) {

View file

@ -41,11 +41,13 @@ export async function POST(request: Request) {
const form = sanitizeCompanyInput(rawBody, tenantId) const form = sanitizeCompanyInput(rawBody, tenantId)
const provisioningCode = randomBytes(32).toString("hex") const provisioningCode = randomBytes(32).toString("hex")
const createData = buildCompanyData(form, tenantId)
const company = await prisma.company.create({ const company = await prisma.company.create({
data: { data: {
...(buildCompanyData(form, tenantId) as any), ...createData,
provisioningCode, provisioningCode,
} as any, },
}) })
if (company.provisioningCode) { if (company.provisioningCode) {

View file

@ -13,16 +13,15 @@ import { Card, CardContent } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { ConvexHttpClient } from "convex/browser"
export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) { export function AdminMachineDetailsClient({ tenantId: _tenantId, machineId }: { tenantId: string; machineId: string }) {
const router = useRouter() const router = useRouter()
const single = useQuery( const queryArgs = machineId
(api as any).machines.getById, ? ({ id: machineId as Id<"machines">, includeMetadata: true } as const)
machineId ? ({ id: machineId as Id<"machines">, includeMetadata: true } as const) : ("skip" as const) : "skip"
) as
| Record<string, unknown> const single = useQuery(api.machines.getById, queryArgs)
| null
| undefined
// Fallback via HTTP in caso de o Convex React demorar/ficar preso em loading // Fallback via HTTP in caso de o Convex React demorar/ficar preso em loading
const [fallback, setFallback] = useState<Record<string, unknown> | null>(null) const [fallback, setFallback] = useState<Record<string, unknown> | null>(null)
@ -34,6 +33,27 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
useEffect(() => { useEffect(() => {
if (!shouldLoad) return if (!shouldLoad) return
timer.current = setTimeout(async () => { timer.current = setTimeout(async () => {
try {
// 1) Tenta via Convex direto do browser (independe do servidor)
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
if (convexUrl) {
try {
const http = new ConvexHttpClient(convexUrl)
const data = (await http.query(api.machines.getById, {
id: machineId as Id<"machines">,
includeMetadata: true,
})) as Record<string, unknown> | null
if (data) {
setFallback(data)
setLoadError(null)
return
}
} catch {
// continua para o plano B
}
}
// 2) Plano B: rota do servidor (útil em ambientes sem Convex público)
try { try {
const res = await fetch(`/api/admin/machines/${machineId}/details`, { credentials: "include" }) const res = await fetch(`/api/admin/machines/${machineId}/details`, { credentials: "include" })
if (res.ok) { if (res.ok) {
@ -41,7 +61,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
setFallback(data) setFallback(data)
setLoadError(null) setLoadError(null)
} else { } else {
// Not found (404) ou erro de servidor
let message = `Falha ao carregar (HTTP ${res.status})` let message = `Falha ao carregar (HTTP ${res.status})`
try { try {
const payload = (await res.json()) as Record<string, unknown> const payload = (await res.json()) as Record<string, unknown>
@ -49,10 +68,13 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
message = payload.error message = payload.error
} }
} catch { } catch {
// ignore parse error // ignore
} }
setLoadError(message) setLoadError(message)
} }
} catch {
setLoadError("Erro de rede ao carregar os dados da máquina.")
}
} catch (err) { } catch (err) {
setLoadError("Erro de rede ao carregar os dados da máquina.") setLoadError("Erro de rede ao carregar os dados da máquina.")
} }
@ -60,7 +82,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
return () => { return () => {
if (timer.current) clearTimeout(timer.current) if (timer.current) clearTimeout(timer.current)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldLoad, machineId, retryTick]) }, [shouldLoad, machineId, retryTick])
// Timeout de proteção: se depois de X segundos ainda estiver carregando e sem fallback, mostra erro claro // 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) }, 10_000)
return () => clearTimeout(timeout) return () => clearTimeout(timeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldLoad, machineId, retryTick]) }, [shouldLoad, machineId, retryTick])
const machine: MachinesQueryItem | null = useMemo(() => { 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 if (source === undefined || source === null) return source as null
return normalizeMachineItem(source) return normalizeMachineItem(source)
}, [single, fallback]) }, [single, fallback])
const isLoading = single === undefined && !fallback const isLoading = single === undefined && !fallback && !loadError
const isNotFound = single === null && !fallback const isNotFound = single === null && !fallback
const onRetry = () => { const onRetry = () => {
@ -94,48 +114,6 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
// ignore // ignore
} }
} }
if (isLoading) {
return (
<Card>
<CardContent className="space-y-3 p-6">
<Skeleton className="h-6 w-64" />
<Skeleton className="h-4 w-80" />
<Skeleton className="h-48 w-full" />
</CardContent>
</Card>
)
}
if (isNotFound) {
return (
<Card>
<CardContent className="space-y-3 p-6">
<p className="text-sm font-medium text-red-600">Máquina não encontrada</p>
<p className="text-sm text-muted-foreground">
Verifique o identificador e tente novamente.
</p>
<div className="pt-2">
<Button size="sm" onClick={onRetry}>Tentar novamente</Button>
</div>
</CardContent>
</Card>
)
}
if (loadError && !machine) {
return (
<Card>
<CardContent className="space-y-3 p-6">
<p className="text-sm font-medium text-red-600">Falha ao carregar os dados da máquina</p>
<p className="text-sm text-muted-foreground">{loadError}</p>
<div className="pt-2 flex items-center gap-2">
<Button size="sm" onClick={onRetry}>Tentar novamente</Button>
</div>
</CardContent>
</Card>
)
}
return <MachineDetails machine={machine} /> return <MachineDetails machine={machine} />
} }

View file

@ -7,15 +7,13 @@ import type { Id } from "@/convex/_generated/dataModel"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { useAuth } from "@/lib/auth-client" 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 { convexUserId } = useAuth()
const item = useQuery( const queryArgs = machineId && convexUserId
(api as any).machines.getById, ? ({ id: machineId as Id<"machines">, includeMetadata: false } as const)
machineId ? ({ id: machineId as Id<"machines">, includeMetadata: false } as const) : ("skip" as const) : "skip"
) as
| { hostname: string } const item = useQuery(api.machines.getById, queryArgs)
| null
| undefined
const hostname = useMemo(() => item?.hostname ?? "Detalhe", [item]) const hostname = useMemo(() => item?.hostname ?? "Detalhe", [item])
return ( return (

View file

@ -17,6 +17,8 @@ export type NormalizedCompany = CompanyFormValues & {
updatedAt: string updatedAt: string
} }
type CompanyCreatePayload = Omit<Prisma.CompanyCreateInput, "provisioningCode">
// Local representation of the DB enum to avoid relying on Prisma enum exports // Local representation of the DB enum to avoid relying on Prisma enum exports
type DbCompanyStateRegistrationType = "STANDARD" | "EXEMPT" | "SIMPLES" type DbCompanyStateRegistrationType = "STANDARD" | "EXEMPT" | "SIMPLES"
@ -210,10 +212,7 @@ export function sanitizeCompanyInput(input: unknown, tenantId: string): CompanyF
return normalized return normalized
} }
export function buildCompanyData( export function buildCompanyData(payload: CompanyFormValues, tenantId: string): CompanyCreatePayload {
payload: CompanyFormValues,
tenantId: string
): Record<string, unknown> {
const stateRegistrationType = payload.stateRegistrationType const stateRegistrationType = payload.stateRegistrationType
? STATE_REGISTRATION_TYPE_TO_PRISMA[payload.stateRegistrationType as CompanyStateRegistrationTypeOption] ? STATE_REGISTRATION_TYPE_TO_PRISMA[payload.stateRegistrationType as CompanyStateRegistrationTypeOption]
: null : null
@ -221,7 +220,7 @@ export function buildCompanyData(
const communicationChannels = mergeChannelsWithPrimary(payload) const communicationChannels = mergeChannelsWithPrimary(payload)
const privacyPolicyMetadata = payload.privacyPolicy?.metadata ?? null const privacyPolicyMetadata = payload.privacyPolicy?.metadata ?? null
return { const data: CompanyCreatePayload = {
tenantId, tenantId,
name: payload.name.trim(), name: payload.name.trim(),
slug: payload.slug.trim(), slug: payload.slug.trim(),
@ -267,9 +266,11 @@ export function buildCompanyData(
customFields: payload.customFields, customFields: payload.customFields,
notes: payload.notes ?? null, notes: payload.notes ?? null,
} }
return data
} }
export function normalizeCompany(company: any): NormalizedCompany { export function normalizeCompany(company: Company): NormalizedCompany {
const communicationChannels = normalizeChannels( const communicationChannels = normalizeChannels(
company.communicationChannels as CompanyCommunicationChannels | null | undefined 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 { return {
id: row.id, id: row.id,
tenantId: row.tenantId, tenantId: row.tenantId,
@ -477,7 +478,7 @@ const COMPANY_BASE_SELECT = Prisma.sql`
FROM "Company" FROM "Company"
` `
export async function fetchCompaniesByTenant(tenantId: string): Promise<any[]> { export async function fetchCompaniesByTenant(tenantId: string): Promise<Company[]> {
const rows = await prisma.$queryRaw<RawCompanyRow[]>(Prisma.sql` const rows = await prisma.$queryRaw<RawCompanyRow[]>(Prisma.sql`
${COMPANY_BASE_SELECT} ${COMPANY_BASE_SELECT}
WHERE tenantId = ${tenantId} WHERE tenantId = ${tenantId}
@ -486,7 +487,7 @@ export async function fetchCompaniesByTenant(tenantId: string): Promise<any[]> {
return rows.map(mapRawRowToCompany) return rows.map(mapRawRowToCompany)
} }
export async function fetchCompanyById(id: string): Promise<any | null> { export async function fetchCompanyById(id: string): Promise<Company | null> {
const rows = await prisma.$queryRaw<RawCompanyRow[]>(Prisma.sql` const rows = await prisma.$queryRaw<RawCompanyRow[]>(Prisma.sql`
${COMPANY_BASE_SELECT} ${COMPANY_BASE_SELECT}
WHERE id = ${id} WHERE id = ${id}

1
types/tsconfig-paths-register.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "tsconfig-paths/register"

View file

@ -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.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 process.env.BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL
} }
export {}