Files
MSP-CAS/src/app/(dashboard)/page.tsx

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>
)
}