233 lines
8.2 KiB
TypeScript
233 lines
8.2 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import { useQuery } from "convex/react"
|
|
import { useParams, useRouter } from "next/navigation"
|
|
import { api } from "@/convex/_generated/api"
|
|
import {
|
|
MachineDetails,
|
|
normalizeMachineItem,
|
|
type MachinesQueryItem,
|
|
} from "@/components/admin/machines/admin-machines-overview"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { Button } from "@/components/ui/button"
|
|
import type { Id } from "@/convex/_generated/dataModel"
|
|
import { ConvexHttpClient } from "convex/browser"
|
|
|
|
export function AdminMachineDetailsClient({ tenantId: _tenantId, machineId }: { tenantId: string; machineId?: string }) {
|
|
const router = useRouter()
|
|
const params = useParams<{ id?: string | string[] }>()
|
|
const routeMachineId = Array.isArray(params?.id) ? params?.id[0] : params?.id
|
|
const effectiveMachineId = machineId ?? routeMachineId ?? ""
|
|
|
|
const queryArgs = effectiveMachineId
|
|
? ({ id: effectiveMachineId 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<Record<string, unknown> | null | undefined>(undefined)
|
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
const [retryTick, setRetryTick] = useState(0)
|
|
const shouldLoad = fallback === undefined && Boolean(effectiveMachineId)
|
|
const [isHydrated, setIsHydrated] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setIsHydrated(true)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!shouldLoad) {
|
|
console.debug("[admin-machine-details] Skipping probe", { shouldLoad, machineId: effectiveMachineId, fallback, single })
|
|
} else {
|
|
console.debug("[admin-machine-details] Starting probe", { machineId: effectiveMachineId, retryTick })
|
|
}
|
|
}, [shouldLoad, effectiveMachineId, fallback, single, retryTick])
|
|
|
|
useEffect(() => {
|
|
if (!shouldLoad) return
|
|
let cancelled = false
|
|
|
|
const probe = async () => {
|
|
console.debug("[admin-machine-details] Probe invocation", { machineId: effectiveMachineId, retryTick })
|
|
try {
|
|
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
|
if (convexUrl) {
|
|
try {
|
|
console.debug("[admin-machine-details] Convex probe begin", { machineId: effectiveMachineId, convexUrl })
|
|
const http = new ConvexHttpClient(convexUrl)
|
|
const data = (await http.query(api.machines.getById, {
|
|
id: effectiveMachineId as Id<"machines">,
|
|
includeMetadata: true,
|
|
})) as Record<string, unknown> | null
|
|
|
|
if (cancelled) return
|
|
|
|
if (data) {
|
|
console.info("[admin-machine-details] Convex query succeeded", { machineId: effectiveMachineId })
|
|
setFallback(data)
|
|
setLoadError(null)
|
|
return
|
|
}
|
|
|
|
if (data === null) {
|
|
console.info("[admin-machine-details] Convex query returned null", { machineId: effectiveMachineId })
|
|
setFallback(null)
|
|
setLoadError(null)
|
|
return
|
|
}
|
|
} catch (err) {
|
|
if (cancelled) return
|
|
console.warn("[admin-machine-details] Convex probe failed, falling back to API route", { machineId: effectiveMachineId, err })
|
|
}
|
|
}
|
|
|
|
try {
|
|
console.debug("[admin-machine-details] HTTP fallback begin", { machineId: effectiveMachineId })
|
|
const res = await fetch(`/api/admin/machines/${effectiveMachineId}/details`, {
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
})
|
|
|
|
if (cancelled) return
|
|
|
|
let payload: Record<string, unknown> | null = null
|
|
try {
|
|
payload = (await res.json()) as Record<string, unknown> | null
|
|
} catch {
|
|
payload = null
|
|
}
|
|
|
|
if (res.ok) {
|
|
console.info("[admin-machine-details] HTTP fallback succeeded", { machineId: effectiveMachineId })
|
|
setFallback(payload ?? null)
|
|
setLoadError(null)
|
|
return
|
|
}
|
|
|
|
const message =
|
|
typeof payload?.error === "string" && payload.error
|
|
? payload.error
|
|
: `Falha ao carregar (HTTP ${res.status})`
|
|
|
|
if (res.status === 404) {
|
|
console.info("[admin-machine-details] HTTP fallback returned 404", { machineId: effectiveMachineId })
|
|
setFallback(null)
|
|
setLoadError(null)
|
|
} else {
|
|
console.error("[admin-machine-details] HTTP fallback failed", { machineId: effectiveMachineId, status: res.status, message })
|
|
setLoadError(message)
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
console.error("[admin-machine-details] API fallback fetch failed", err)
|
|
setLoadError("Erro de rede ao carregar os dados da máquina.")
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
console.error("[admin-machine-details] Unexpected probe failure", err)
|
|
setLoadError("Erro de rede ao carregar os dados da máquina.")
|
|
}
|
|
}
|
|
}
|
|
|
|
probe().catch((err) => {
|
|
if (!cancelled) {
|
|
console.error("[admin-machine-details] Probe promise rejected", err)
|
|
setLoadError("Erro de rede ao carregar os dados da máquina.")
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
console.debug("[admin-machine-details] Cancelling probe", { machineId: effectiveMachineId })
|
|
cancelled = true
|
|
}
|
|
}, [shouldLoad, effectiveMachineId, retryTick])
|
|
|
|
// Timeout de proteção: se depois de X segundos ainda estiver carregando e sem fallback, mostra erro claro
|
|
useEffect(() => {
|
|
if (!shouldLoad) return
|
|
const timeout = setTimeout(() => {
|
|
setLoadError((error) =>
|
|
error ?? "Tempo esgotado ao consultar os dados (Convex). Verifique sua conexão e tente novamente."
|
|
)
|
|
}, 10_000)
|
|
return () => clearTimeout(timeout)
|
|
}, [shouldLoad, effectiveMachineId, retryTick])
|
|
|
|
const machine: MachinesQueryItem | null = useMemo(() => {
|
|
const source = single ?? (fallback === undefined ? undefined : fallback)
|
|
if (source === undefined || source === null) return source as null
|
|
return normalizeMachineItem(source)
|
|
}, [single, fallback])
|
|
const isLoading = single === undefined && fallback === undefined && !loadError
|
|
const isNotFound = (single === null || fallback === null) && !loadError
|
|
|
|
if (!isHydrated) {
|
|
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>
|
|
)
|
|
}
|
|
|
|
const onRetry = () => {
|
|
setLoadError(null)
|
|
setFallback(undefined)
|
|
setRetryTick((t) => t + 1)
|
|
// força revalidação de RSC/convex subscription
|
|
try {
|
|
router.refresh()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
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>
|
|
)
|
|
}
|
|
|
|
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 flex items-center gap-2">
|
|
<Button size="sm" onClick={onRetry}>Recarregar</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return <MachineDetails machine={machine} />
|
|
}
|