Atualiza portal e admin com bloqueio de máquinas desativadas

This commit is contained in:
Esdras Renan 2025-10-18 00:02:15 -03:00
parent e5085962e9
commit 630110bf3a
31 changed files with 1756 additions and 244 deletions

View file

@ -1,6 +1,7 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState, useTransition, useId } from "react"
import Link from "next/link"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { useQuery } from "convex/react"
@ -11,6 +12,7 @@ import {
IconCopy,
IconDotsVertical,
IconCheck,
IconDeviceDesktop,
IconPencil,
IconRefresh,
IconSearch,
@ -73,6 +75,11 @@ type MachineSummary = {
hostname: string
status: string | null
lastHeartbeatAt: number | null
isActive?: boolean | null
authEmail?: string | null
osName?: string | null
osVersion?: string | null
architecture?: string | null
}
export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: Company[] }) {
@ -84,6 +91,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
const [deleteId, setDeleteId] = useState<string | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const [searchTerm, setSearchTerm] = useState("")
const [machinesDialog, setMachinesDialog] = useState<{ companyId: string; name: string } | null>(null)
const isMobile = useIsMobile()
const nameId = useId()
@ -111,6 +119,10 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
if (!editingId) return []
return machinesByCompanyId.get(editingId) ?? []
}, [machinesByCompanyId, editingId])
const machinesDialogList = useMemo(() => {
if (!machinesDialog) return []
return machinesByCompanyId.get(machinesDialog.companyId) ?? []
}, [machinesByCompanyId, machinesDialog])
const resetForm = () => setForm({})
@ -575,7 +587,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
</div>
<div className="flex flex-wrap items-center gap-2">
{companyMachines.slice(0, 3).map((machine) => {
const variant = getMachineStatusVariant(machine.status)
const variant = getMachineStatusVariant(machine.isActive === false ? "deactivated" : machine.status)
return (
<Tooltip key={machine.id}>
<TooltipTrigger asChild>
@ -661,15 +673,15 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
)}
</div>
) : (
<div className="overflow-x-auto">
<div className="overflow-hidden rounded-lg border border-slate-200">
<Table className="min-w-full table-fixed text-sm">
<TableHeader>
<TableRow className="border-slate-100/80 dark:border-slate-800/60">
<TableHead className="w-[30%] min-w-[220px] pl-6 text-slate-500 dark:text-slate-300">Empresa</TableHead>
<TableHead className="w-[22%] min-w-[180px] pl-4 text-slate-500 dark:text-slate-300">Provisionamento</TableHead>
<TableHead className="w-[18%] min-w-[160px] pl-12 text-slate-500 dark:text-slate-300">Cliente avulso</TableHead>
<TableHead className="w-[20%] min-w-[170px] pl-12 text-slate-500 dark:text-slate-300">Uso e alertas</TableHead>
<TableHead className="w-[10%] min-w-[90px] pr-6 text-right text-slate-500 dark:text-slate-300">Ações</TableHead>
<TableHeader className="sticky top-0 z-10 bg-muted/60 backdrop-blur supports-[backdrop-filter]:bg-muted/40">
<TableRow className="border-b border-slate-200 dark:border-slate-800/60 [&_th]:h-10 [&_th]:text-xs [&_th]:font-medium [&_th]:uppercase [&_th]:tracking-wide [&_th]:text-muted-foreground [&_th:first-child]:rounded-tl-lg [&_th:last-child]:rounded-tr-lg">
<TableHead className="w-[30%] min-w-[220px] pl-6">Empresa</TableHead>
<TableHead className="w-[22%] min-w-[180px] pl-4">Provisionamento</TableHead>
<TableHead className="w-[18%] min-w-[160px] pl-12">Cliente avulso</TableHead>
<TableHead className="w-[20%] min-w-[170px] pl-12">Uso e alertas</TableHead>
<TableHead className="w-[10%] min-w-[90px] pr-6 text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -683,6 +695,8 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
? formatDistanceToNow(alertInfo.createdAt, { addSuffix: true, locale: ptBR })
: null
const formattedPhone = formatPhoneDisplay(company.phone)
const companyMachines = machinesByCompanyId.get(company.id) ?? []
const machineCount = companyMachines.length
return (
<TableRow
key={company.id}
@ -710,6 +724,12 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
{company.contractedHoursPerMonth}h/mês
</Badge>
) : null}
<Badge
variant="outline"
className="border-slate-200 bg-white text-[11px] font-medium dark:border-slate-800 dark:bg-slate-900"
>
{machineCount} máquina{machineCount === 1 ? "" : "s"}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="text-[12px] font-medium text-slate-500 dark:text-slate-400">
@ -803,6 +823,16 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
)}
</TableCell>
<TableCell className="pr-6 text-right align-top">
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="ghost"
size="sm"
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-100"
onClick={() => setMachinesDialog({ companyId: company.id, name: company.name })}
>
<IconDeviceDesktop className="size-4" /> Ver máquinas
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="ml-auto">
@ -829,6 +859,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
)
@ -845,6 +876,54 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
</div>
)}
</CardContent>
<Dialog open={!!machinesDialog} onOpenChange={(open) => { if (!open) setMachinesDialog(null) }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Máquinas {machinesDialog?.name ?? ""}</DialogTitle>
</DialogHeader>
{machinesDialogList.length === 0 ? (
<p className="text-sm text-muted-foreground">Nenhuma máquina vinculada a esta empresa.</p>
) : (
<ul className="space-y-3">
{machinesDialogList.map((machine) => {
const statusKey = machine.isActive === false ? "deactivated" : machine.status
const statusVariant = getMachineStatusVariant(statusKey)
return (
<li key={machine.id} className="flex flex-col gap-2 rounded-xl border border-slate-200 bg-slate-50/60 px-4 py-3 text-sm text-neutral-700">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-neutral-900">{machine.hostname}</p>
<p className="text-xs text-neutral-500">{machine.authEmail ?? "Sem e-mail definido"}</p>
</div>
<Badge variant="outline" className={cn("h-7 px-3 text-xs font-medium", statusVariant.className)}>
{statusVariant.label}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs text-neutral-500">
<span>{machine.osName ?? "SO desconhecido"}</span>
{machine.osVersion ? <span className="text-neutral-400"></span> : null}
{machine.osVersion ? <span>{machine.osVersion}</span> : null}
{machine.architecture ? (
<span className="rounded-full bg-white px-2 py-0.5 text-[11px] font-medium text-neutral-600 shadow-sm">
{machine.architecture.toUpperCase()}
</span>
) : null}
</div>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm" className="text-xs">
<Link href={`/admin/machines/${machine.id}`}>Ver detalhes</Link>
</Button>
<span className="text-xs text-neutral-500">
Último sinal: {formatRelativeTimestamp(machine.lastHeartbeatAt) ?? "nunca"}
</span>
</div>
</li>
)
})}
</ul>
)}
</DialogContent>
</Dialog>
</Card>
</TooltipProvider>
@ -892,6 +971,7 @@ const MACHINE_STATUS_VARIANTS: Record<string, { label: string; className: string
stale: { label: "Sem sinal", className: "border-slate-300 bg-slate-200/60 text-slate-700" },
maintenance: { label: "Manutenção", className: "border-amber-200 bg-amber-500/10 text-amber-600" },
blocked: { label: "Bloqueada", className: "border-orange-200 bg-orange-500/10 text-orange-600" },
deactivated: { label: "Desativada", className: "border-slate-300 bg-slate-100 text-slate-600" },
unknown: { label: "Desconhecida", className: "border-slate-200 bg-slate-100 text-slate-600" },
}
@ -902,7 +982,7 @@ function getMachineStatusVariant(status?: string | null) {
function summarizeStatus(machines: MachineSummary[]): Record<string, number> {
return machines.reduce<Record<string, number>>((acc, machine) => {
const normalized = (machine.status ?? "unknown").toLowerCase()
const normalized = (machine.isActive === false ? "deactivated" : machine.status ?? "unknown").toLowerCase()
acc[normalized] = (acc[normalized] ?? 0) + 1
return acc
}, {})