Atualiza portal e admin com bloqueio de máquinas desativadas
This commit is contained in:
parent
e5085962e9
commit
630110bf3a
31 changed files with 1756 additions and 244 deletions
|
|
@ -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
|
||||
}, {})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue