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:
parent
eee0f432e7
commit
c640e288b1
8 changed files with 76 additions and 90 deletions
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -35,23 +34,46 @@ export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: s
|
||||||
if (!shouldLoad) return
|
if (!shouldLoad) return
|
||||||
timer.current = setTimeout(async () => {
|
timer.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/machines/${machineId}/details`, { credentials: "include" })
|
// 1) Tenta via Convex direto do browser (independe do servidor)
|
||||||
if (res.ok) {
|
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||||
const data = (await res.json()) as Record<string, unknown>
|
if (convexUrl) {
|
||||||
setFallback(data)
|
|
||||||
setLoadError(null)
|
|
||||||
} else {
|
|
||||||
// Not found (404) ou erro de servidor
|
|
||||||
let message = `Falha ao carregar (HTTP ${res.status})`
|
|
||||||
try {
|
try {
|
||||||
const payload = (await res.json()) as Record<string, unknown>
|
const http = new ConvexHttpClient(convexUrl)
|
||||||
if (typeof payload?.error === "string" && payload.error) {
|
const data = (await http.query(api.machines.getById, {
|
||||||
message = payload.error
|
id: machineId as Id<"machines">,
|
||||||
|
includeMetadata: true,
|
||||||
|
})) as Record<string, unknown> | null
|
||||||
|
if (data) {
|
||||||
|
setFallback(data)
|
||||||
|
setLoadError(null)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} 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<string, unknown>
|
||||||
|
setFallback(data)
|
||||||
|
setLoadError(null)
|
||||||
|
} else {
|
||||||
|
let message = `Falha ao carregar (HTTP ${res.status})`
|
||||||
|
try {
|
||||||
|
const payload = (await res.json()) as Record<string, unknown>
|
||||||
|
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) {
|
} 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} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
1
types/tsconfig-paths-register.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
declare module "tsconfig-paths/register"
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue