373 lines
10 KiB
TypeScript
373 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import { RefreshCw, Grid, List, Filter } from 'lucide-react'
|
|
import KPICards from '@/components/dashboard/KPICards'
|
|
import DeviceGrid from '@/components/dashboard/DeviceGrid'
|
|
import AlertsFeed from '@/components/dashboard/AlertsFeed'
|
|
import { useSelectedClient } from '@/components/providers/SelectedClientProvider'
|
|
import { cn } from '@/lib/utils'
|
|
import { trpc } from '@/lib/trpc-client'
|
|
|
|
type DeviceForGrid = {
|
|
id: string
|
|
nombre: string
|
|
tipo: string
|
|
estado: string
|
|
ip?: string | null
|
|
sistemaOperativo?: string | null
|
|
lastSeen?: Date | null
|
|
cpuUsage?: number | null
|
|
ramUsage?: number | null
|
|
cliente?: { nombre: string }
|
|
}
|
|
|
|
type DashboardAlert = {
|
|
id: string
|
|
severidad: 'INFO' | 'WARNING' | 'CRITICAL'
|
|
estado: 'ACTIVA' | 'RECONOCIDA' | 'RESUELTA'
|
|
titulo: string
|
|
mensaje: string
|
|
createdAt: Date
|
|
dispositivo: { nombre: string }
|
|
cliente: { nombre: string }
|
|
}
|
|
|
|
// Mock data - en produccion vendria de la API
|
|
const mockStats = {
|
|
totalDispositivos: 127,
|
|
dispositivosOnline: 98,
|
|
dispositivosOffline: 24,
|
|
dispositivosAlerta: 5,
|
|
alertasActivas: 8,
|
|
alertasCriticas: 2,
|
|
sesionesActivas: 3,
|
|
}
|
|
|
|
const mockDevices = [
|
|
{
|
|
id: '1',
|
|
nombre: 'SRV-PRINCIPAL',
|
|
tipo: 'SERVIDOR',
|
|
estado: 'ONLINE',
|
|
ip: '192.168.1.10',
|
|
sistemaOperativo: 'Windows Server 2022',
|
|
lastSeen: new Date(),
|
|
cpuUsage: 45,
|
|
ramUsage: 72,
|
|
},
|
|
{
|
|
id: '2',
|
|
nombre: 'PC-ADMIN-01',
|
|
tipo: 'PC',
|
|
estado: 'ONLINE',
|
|
ip: '192.168.1.101',
|
|
sistemaOperativo: 'Windows 11 Pro',
|
|
lastSeen: new Date(),
|
|
cpuUsage: 23,
|
|
ramUsage: 56,
|
|
},
|
|
{
|
|
id: '3',
|
|
nombre: 'LAPTOP-VENTAS',
|
|
tipo: 'LAPTOP',
|
|
estado: 'ALERTA',
|
|
ip: '192.168.1.105',
|
|
sistemaOperativo: 'Windows 11 Pro',
|
|
lastSeen: new Date(Date.now() - 1000 * 60 * 5),
|
|
cpuUsage: 95,
|
|
ramUsage: 88,
|
|
},
|
|
{
|
|
id: '4',
|
|
nombre: 'ROUTER-PRINCIPAL',
|
|
tipo: 'ROUTER',
|
|
estado: 'ONLINE',
|
|
ip: '192.168.1.1',
|
|
sistemaOperativo: 'RouterOS 7.12',
|
|
lastSeen: new Date(),
|
|
cpuUsage: null,
|
|
ramUsage: null,
|
|
},
|
|
{
|
|
id: '5',
|
|
nombre: 'SW-CORE-01',
|
|
tipo: 'SWITCH',
|
|
estado: 'ONLINE',
|
|
ip: '192.168.1.2',
|
|
sistemaOperativo: 'Cisco IOS',
|
|
lastSeen: new Date(),
|
|
cpuUsage: null,
|
|
ramUsage: null,
|
|
},
|
|
{
|
|
id: '6',
|
|
nombre: 'CELULAR-GERENTE',
|
|
tipo: 'CELULAR',
|
|
estado: 'ONLINE',
|
|
ip: null,
|
|
sistemaOperativo: 'Android 14',
|
|
lastSeen: new Date(),
|
|
cpuUsage: null,
|
|
ramUsage: null,
|
|
},
|
|
{
|
|
id: '7',
|
|
nombre: 'SRV-BACKUP',
|
|
tipo: 'SERVIDOR',
|
|
estado: 'OFFLINE',
|
|
ip: '192.168.1.11',
|
|
sistemaOperativo: 'Ubuntu 22.04',
|
|
lastSeen: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
|
cpuUsage: null,
|
|
ramUsage: null,
|
|
},
|
|
{
|
|
id: '8',
|
|
nombre: 'AP-OFICINA-01',
|
|
tipo: 'AP',
|
|
estado: 'ONLINE',
|
|
ip: '192.168.1.50',
|
|
sistemaOperativo: 'UniFi AP',
|
|
lastSeen: new Date(),
|
|
cpuUsage: null,
|
|
ramUsage: null,
|
|
},
|
|
]
|
|
|
|
const mockAlerts = [
|
|
{
|
|
id: '1',
|
|
severidad: 'CRITICAL' as const,
|
|
estado: 'ACTIVA' as const,
|
|
titulo: 'Servidor de backup offline',
|
|
mensaje: 'El servidor SRV-BACKUP no responde desde hace 2 horas',
|
|
createdAt: new Date(Date.now() - 1000 * 60 * 120),
|
|
dispositivo: { nombre: 'SRV-BACKUP' },
|
|
cliente: { nombre: 'Cliente A' },
|
|
},
|
|
{
|
|
id: '2',
|
|
severidad: 'WARNING' as const,
|
|
estado: 'ACTIVA' as const,
|
|
titulo: 'CPU alta',
|
|
mensaje: 'Uso de CPU al 95% en LAPTOP-VENTAS',
|
|
createdAt: new Date(Date.now() - 1000 * 60 * 15),
|
|
dispositivo: { nombre: 'LAPTOP-VENTAS' },
|
|
cliente: { nombre: 'Cliente A' },
|
|
},
|
|
{
|
|
id: '3',
|
|
severidad: 'INFO' as const,
|
|
estado: 'RECONOCIDA' as const,
|
|
titulo: 'Actualizacion disponible',
|
|
mensaje: 'Windows Update pendiente en PC-ADMIN-01',
|
|
createdAt: new Date(Date.now() - 1000 * 60 * 60),
|
|
dispositivo: { nombre: 'PC-ADMIN-01' },
|
|
cliente: { nombre: 'Cliente A' },
|
|
},
|
|
]
|
|
|
|
const DEVICES_LIMIT = 12
|
|
|
|
export default function DashboardPage() {
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
|
const utils = trpc.useUtils()
|
|
const { selectedClientId } = useSelectedClient()
|
|
const clienteId = selectedClientId ?? undefined
|
|
|
|
const statsQuery = trpc.clientes.dashboardStats.useQuery(
|
|
{ clienteId },
|
|
{ refetchOnWindowFocus: false }
|
|
)
|
|
const stats = statsQuery.data ?? mockStats
|
|
|
|
const alertsQuery = trpc.alertas.list.useQuery(
|
|
{ page: 1, limit: 25, clienteId },
|
|
{ refetchOnWindowFocus: false }
|
|
)
|
|
const alerts: DashboardAlert[] = useMemo(() => {
|
|
const list = alertsQuery.data?.alertas ?? []
|
|
return list.map((a) => ({
|
|
id: a.id,
|
|
severidad: a.severidad,
|
|
estado: a.estado,
|
|
titulo: a.titulo,
|
|
mensaje: a.mensaje,
|
|
createdAt: a.createdAt instanceof Date ? a.createdAt : new Date(a.createdAt),
|
|
dispositivo: a.dispositivo ? { nombre: a.dispositivo.nombre } : { nombre: '—' },
|
|
cliente: { nombre: a.cliente.nombre },
|
|
}))
|
|
}, [alertsQuery.data])
|
|
|
|
const acknowledgeMutation = trpc.alertas.reconocer.useMutation({
|
|
onSuccess: () => {
|
|
utils.alertas.list.invalidate()
|
|
utils.clientes.dashboardStats.invalidate()
|
|
},
|
|
})
|
|
const resolveMutation = trpc.alertas.resolver.useMutation({
|
|
onSuccess: () => {
|
|
utils.alertas.list.invalidate()
|
|
utils.clientes.dashboardStats.invalidate()
|
|
},
|
|
})
|
|
|
|
const equiposQuery = trpc.equipos.list.useQuery(
|
|
{ page: 1, limit: DEVICES_LIMIT, clienteId },
|
|
{ refetchOnWindowFocus: false }
|
|
)
|
|
const redQuery = trpc.red.list.useQuery(
|
|
{ page: 1, limit: DEVICES_LIMIT, clienteId },
|
|
{ refetchOnWindowFocus: false }
|
|
)
|
|
const celularesQuery = trpc.celulares.list.useQuery(
|
|
{ page: 1, limit: DEVICES_LIMIT, clienteId },
|
|
{ refetchOnWindowFocus: false }
|
|
)
|
|
|
|
const devices: DeviceForGrid[] = useMemo(() => {
|
|
const eq = equiposQuery.data?.dispositivos ?? []
|
|
const rd = redQuery.data?.dispositivos ?? []
|
|
const cel = celularesQuery.data?.dispositivos ?? []
|
|
const all = [...eq, ...rd, ...cel]
|
|
return all.map((d) => ({
|
|
id: d.id,
|
|
nombre: d.nombre,
|
|
tipo: d.tipo,
|
|
estado: d.estado,
|
|
ip: d.ip ?? null,
|
|
sistemaOperativo: d.sistemaOperativo ?? null,
|
|
lastSeen: d.lastSeen ?? null,
|
|
cpuUsage: d.cpuUsage ?? null,
|
|
ramUsage: d.ramUsage ?? null,
|
|
cliente: d.cliente ? { nombre: d.cliente.nombre } : undefined,
|
|
}))
|
|
}, [equiposQuery.data, redQuery.data, celularesQuery.data])
|
|
|
|
const devicesLoading =
|
|
equiposQuery.isLoading || redQuery.isLoading || celularesQuery.isLoading
|
|
const isRefreshing =
|
|
statsQuery.isFetching ||
|
|
alertsQuery.isFetching ||
|
|
equiposQuery.isFetching ||
|
|
redQuery.isFetching ||
|
|
celularesQuery.isFetching
|
|
|
|
const handleRefresh = async () => {
|
|
await Promise.all([
|
|
statsQuery.refetch(),
|
|
alertsQuery.refetch(),
|
|
equiposQuery.refetch(),
|
|
redQuery.refetch(),
|
|
celularesQuery.refetch(),
|
|
])
|
|
}
|
|
|
|
const handleDeviceAction = (deviceId: string, action: string) => {
|
|
console.log(`Action ${action} on device ${deviceId}`)
|
|
// TODO: Implementar acciones
|
|
}
|
|
|
|
const handleAcknowledgeAlert = (alertId: string) => {
|
|
acknowledgeMutation.mutate({ id: alertId })
|
|
}
|
|
|
|
const handleResolveAlert = (alertId: string) => {
|
|
resolveMutation.mutate({ id: alertId })
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
<p className="text-gray-500">Vision general del sistema</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="btn btn-secondary"
|
|
disabled={isRefreshing}
|
|
>
|
|
<RefreshCw className={cn('w-4 h-4 mr-2', isRefreshing && 'animate-spin')} />
|
|
Actualizar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<KPICards stats={stats} />
|
|
|
|
{/* Main content */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Devices */}
|
|
<div className="lg:col-span-2 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-medium">Dispositivos</h2>
|
|
<div className="flex items-center gap-2">
|
|
<button className="btn btn-ghost btn-sm">
|
|
<Filter className="w-4 h-4 mr-1" />
|
|
Filtrar
|
|
</button>
|
|
<div className="flex border border-dark-100 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className={cn(
|
|
'p-2 transition-colors',
|
|
viewMode === 'grid' ? 'bg-dark-100 text-primary-400' : 'text-gray-500'
|
|
)}
|
|
>
|
|
<Grid className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={cn(
|
|
'p-2 transition-colors',
|
|
viewMode === 'list' ? 'bg-dark-100 text-primary-400' : 'text-gray-500'
|
|
)}
|
|
>
|
|
<List className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{devicesLoading ? (
|
|
<div className="rounded-lg border border-dark-100 bg-dark-400 p-8 text-center text-gray-400">
|
|
Cargando dispositivos...
|
|
</div>
|
|
) : devices.length === 0 ? (
|
|
<div className="rounded-lg border border-dark-100 bg-dark-400 p-8 text-center text-gray-400">
|
|
No hay dispositivos. Agregue clientes y sincronice con MeshCentral, LibreNMS o Headwind.
|
|
</div>
|
|
) : (
|
|
<DeviceGrid
|
|
devices={devices}
|
|
viewMode={viewMode}
|
|
onAction={handleDeviceAction}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Alerts */}
|
|
<div>
|
|
{alertsQuery.isLoading ? (
|
|
<div className="card p-8 text-center text-gray-400">
|
|
Cargando alertas...
|
|
</div>
|
|
) : (
|
|
<AlertsFeed
|
|
alerts={alerts}
|
|
onAcknowledge={handleAcknowledgeAlert}
|
|
onResolve={handleResolveAlert}
|
|
maxItems={10}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|