diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx
index 12e6310..106775a 100644
--- a/src/app/(dashboard)/page.tsx
+++ b/src/app/(dashboard)/page.tsx
@@ -1,178 +1,29 @@
'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 { useMemo } from 'react'
+import Link from 'next/link'
+import { Monitor, CheckCircle, XCircle, AlertTriangle, Plus } from 'lucide-react'
import { useSelectedClient } from '@/components/providers/SelectedClientProvider'
-import { cn } from '@/lib/utils'
import { trpc } from '@/lib/trpc-client'
+import SummaryCard from '@/components/dashboard/SummaryCard'
+import DeviceStatusChart from '@/components/dashboard/DeviceStatusChart'
+import RecentActivityList from '@/components/dashboard/RecentActivityList'
+import SystemHealthCard from '@/components/dashboard/SystemHealthCard'
+import QuickConnectionCard from '@/components/dashboard/QuickConnectionCard'
+import {
+ MOCK_DASHBOARD_SECONDARY,
+ MOCK_SYSTEM_HEALTH,
+ MOCK_QUICK_CONNECTIONS,
+} from '@/mocks/dashboardData'
+import type {
+ QuickConnectionItem,
+ QuickConnectionStatus,
+ RecentActivityItem,
+} from '@/mocks/dashboardData'
-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
+const DEVICES_LIMIT = 8
export default function DashboardPage() {
- const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
- const utils = trpc.useUtils()
const { selectedClientId } = useSelectedClient()
const clienteId = selectedClientId ?? undefined
@@ -180,38 +31,6 @@ export default function DashboardPage() {
{ 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 },
@@ -226,147 +45,155 @@ export default function DashboardPage() {
{ 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,
+ const alertsQuery = trpc.alertas.list.useQuery(
+ { page: 1, limit: 15, clienteId },
+ { refetchOnWindowFocus: false }
+ )
+
+ const recentActivity: RecentActivityItem[] = useMemo(() => {
+ const list = alertsQuery.data?.alertas ?? []
+ return list.map((a) => ({
+ id: a.id,
+ type: 'alert' as const,
+ description: a.titulo,
+ deviceName: a.dispositivo?.nombre ?? '—',
+ timestamp: a.createdAt instanceof Date ? a.createdAt : new Date(a.createdAt),
+ severity: a.severidad,
}))
- }, [equiposQuery.data, redQuery.data, celularesQuery.data])
+ }, [alertsQuery.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 stats = statsQuery.data ?? {
+ totalDispositivos: 0,
+ dispositivosOnline: 0,
+ dispositivosOffline: 0,
+ dispositivosAlerta: 0,
+ alertasActivas: 0,
+ alertasCriticas: 0,
+ sesionesActivas: 0,
}
- const handleDeviceAction = (deviceId: string, action: string) => {
- console.log(`Action ${action} on device ${deviceId}`)
- // TODO: Implementar acciones
+ const deviceStatusBreakdown = {
+ online: stats.dispositivosOnline,
+ offline: stats.dispositivosOffline,
+ advertencia: stats.dispositivosAlerta,
}
- const handleAcknowledgeAlert = (alertId: string) => {
- acknowledgeMutation.mutate({ id: alertId })
- }
+ const allDevices = [
+ ...(equiposQuery.data?.dispositivos ?? []),
+ ...(redQuery.data?.dispositivos ?? []),
+ ...(celularesQuery.data?.dispositivos ?? []),
+ ].slice(0, DEVICES_LIMIT)
- const handleResolveAlert = (alertId: string) => {
- resolveMutation.mutate({ id: alertId })
- }
+ const quickConnections: QuickConnectionItem[] =
+ allDevices.length > 0
+ ? allDevices.map((d) => ({
+ id: d.id,
+ name: d.nombre,
+ status: mapEstadoToQuickStatus(d.estado),
+ }))
+ : MOCK_QUICK_CONNECTIONS
return (
- {/* Header */}
-
+
- {/* KPI Cards */}
-
+
+ }
+ iconBgClass="bg-primary-900/30"
+ iconColorClass="text-primary-400"
+ />
+ 0
+ ? `${Math.round((stats.dispositivosOnline / stats.totalDispositivos) * 100)}% disponibilidad`
+ : MOCK_DASHBOARD_SECONDARY.online
+ }
+ icon={}
+ iconBgClass="bg-success/20"
+ iconColorClass="text-success"
+ />
+ }
+ iconBgClass="bg-gray-500/20"
+ iconColorClass="text-gray-400"
+ />
+ }
+ iconBgClass="bg-warning/20"
+ iconColorClass="text-warning"
+ />
+
- {/* Main content */}
- {/* Devices */}
-
-
-
Dispositivos
-
-
-
-
-
-
-
-
-
- {devicesLoading ? (
-
- Cargando dispositivos...
-
- ) : devices.length === 0 ? (
-
- No hay dispositivos. Agregue clientes y sincronice con MeshCentral, LibreNMS o Headwind.
-
- ) : (
-
- )}
-
-
- {/* Alerts */}
-
- {alertsQuery.isLoading ? (
-
- Cargando alertas...
-
- ) : (
-
- )}
-
+
+
+
+
+ Salud del Sistema
+
+
+
+
+
+
+
+
+ Conexión Rápida
+
+ {quickConnections.map((item) => (
+ {
+ // TODO: router.push(`/devices?id=${id}`) or open device detail modal
+ }}
+ />
+ ))}
+
+
)
}
+
+function mapEstadoToQuickStatus(estado: string): QuickConnectionStatus {
+ const u = estado?.toUpperCase()
+ if (u === 'ONLINE') return 'online'
+ if (u === 'ALERTA') return 'advertencia'
+ return 'offline'
+}
diff --git a/src/components/dashboard/DeviceStatusChart.tsx b/src/components/dashboard/DeviceStatusChart.tsx
new file mode 100644
index 0000000..7e53280
--- /dev/null
+++ b/src/components/dashboard/DeviceStatusChart.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
+import type { DeviceStatusBreakdown } from '@/mocks/dashboardData'
+
+interface DeviceStatusChartProps {
+ data: DeviceStatusBreakdown
+}
+
+const COLORS = {
+ online: '#22c55e',
+ offline: '#64748b',
+ advertencia: '#eab308',
+}
+
+export default function DeviceStatusChart({ data }: DeviceStatusChartProps) {
+ const total = data.online + data.offline + data.advertencia
+ const segments = [
+ { name: 'En Línea', value: data.online, color: COLORS.online },
+ { name: 'Fuera de Línea', value: data.offline, color: COLORS.offline },
+ { name: 'Advertencia', value: data.advertencia, color: COLORS.advertencia },
+ ].filter((s) => s.value > 0)
+
+ if (total === 0) {
+ return (
+
+
Estado de Dispositivos
+
+ Sin datos
+
+
+ )
+ }
+
+ return (
+
+
Estado de Dispositivos
+
+
+
+
+
+ {segments.map((entry, index) => (
+ |
+ ))}
+
+ [
+ `${value} (${total > 0 ? Math.round((value / total) * 100) : 0}%)`,
+ '',
+ ]}
+ />
+
+
+
+
+ {segments.map((s) => (
+ -
+
+
+ {s.name}
+
+
+ {s.value} ({total > 0 ? Math.round((s.value / total) * 100) : 0}%)
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/dashboard/QuickConnectionCard.tsx b/src/components/dashboard/QuickConnectionCard.tsx
new file mode 100644
index 0000000..92f09b8
--- /dev/null
+++ b/src/components/dashboard/QuickConnectionCard.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import type { QuickConnectionItem, QuickConnectionStatus } from '@/mocks/dashboardData'
+
+interface QuickConnectionCardProps {
+ item: QuickConnectionItem
+ onClick?: (id: string) => void
+}
+
+const statusConfig: Record<
+ QuickConnectionStatus,
+ { dot: string; label: string; text: string }
+> = {
+ online: { dot: 'bg-success', label: 'En línea', text: 'text-success' },
+ advertencia: { dot: 'bg-warning', label: 'Advertencia', text: 'text-warning' },
+ offline: { dot: 'bg-gray-500', label: 'Fuera de línea', text: 'text-gray-500' },
+}
+
+export default function QuickConnectionCard({ item, onClick }: QuickConnectionCardProps) {
+ const config = statusConfig[item.status]
+ return (
+
+ )
+}
diff --git a/src/components/dashboard/RecentActivityList.tsx b/src/components/dashboard/RecentActivityList.tsx
new file mode 100644
index 0000000..da5d0e0
--- /dev/null
+++ b/src/components/dashboard/RecentActivityList.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import Link from 'next/link'
+import {
+ LogIn,
+ AlertTriangle,
+ Link2,
+ Link2Off,
+} from 'lucide-react'
+import { cn, formatRelativeTime } from '@/lib/utils'
+import type { RecentActivityItem, RecentActivityType } from '@/mocks/dashboardData'
+import type { AlertSeverity } from '@/mocks/dashboardData'
+
+function formatRelative(date: Date): string {
+ const s = formatRelativeTime(date)
+ return s === 'ahora' ? 'Hace un momento' : s.replace(/^hace/, 'Hace')
+}
+
+interface RecentActivityListProps {
+ items: RecentActivityItem[]
+ isLoading?: boolean
+}
+
+const severityIconBg: Record = {
+ CRITICAL: 'bg-danger/20 text-danger',
+ WARNING: 'bg-warning/20 text-warning',
+ INFO: 'bg-info/20 text-info',
+}
+
+const typeConfig: Record<
+ RecentActivityType,
+ { icon: React.ReactNode; label: string }
+> = {
+ login: { icon: , label: 'Login' },
+ alert: { icon: , label: 'Alerta' },
+ connection: { icon: , label: 'Conexión' },
+ disconnection: { icon: , label: 'Desconexión' },
+}
+
+export default function RecentActivityList({ items, isLoading }: RecentActivityListProps) {
+ return (
+
+
+
Actividad Reciente
+
+ Ver todas
+
+
+
+ {isLoading ? (
+
+ Cargando...
+
+ ) : items.length === 0 ? (
+
+ No hay alertas recientes
+
+ ) : (
+ items.map((item) => {
+ const config = typeConfig[item.type]
+ const iconBg =
+ item.type === 'alert' && item.severity
+ ? severityIconBg[item.severity]
+ : 'bg-dark-300 text-gray-400'
+ return (
+
+
+ {config.icon}
+
+
+
+ {item.description}
+ {' · '}
+
+ {item.deviceName}
+
+
+
+ {formatRelative(item.timestamp)}
+
+
+
+ )
+ })
+ )}
+
+
+ )
+}
diff --git a/src/components/dashboard/SummaryCard.tsx b/src/components/dashboard/SummaryCard.tsx
new file mode 100644
index 0000000..d7c2bb5
--- /dev/null
+++ b/src/components/dashboard/SummaryCard.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+
+interface SummaryCardProps {
+ title: string
+ value: number
+ secondary?: string
+ icon: React.ReactNode
+ iconBgClass?: string
+ iconColorClass?: string
+}
+
+export default function SummaryCard({
+ title,
+ value,
+ secondary,
+ icon,
+ iconBgClass = 'bg-dark-300',
+ iconColorClass = 'text-primary-400',
+}: SummaryCardProps) {
+ return (
+
+
+
+
{title}
+
{value}
+ {secondary && (
+
{secondary}
+ )}
+
+
+ {icon}
+
+
+
+ )
+}
diff --git a/src/components/dashboard/SystemHealthCard.tsx b/src/components/dashboard/SystemHealthCard.tsx
new file mode 100644
index 0000000..4f851ea
--- /dev/null
+++ b/src/components/dashboard/SystemHealthCard.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import type { SystemHealthMetric } from '@/mocks/dashboardData'
+
+interface SystemHealthCardProps {
+ metric: SystemHealthMetric
+}
+
+const statusBarClass = {
+ healthy: 'bg-success',
+ warning: 'bg-warning',
+ critical: 'bg-danger',
+}
+
+export default function SystemHealthCard({ metric }: SystemHealthCardProps) {
+ const percent = metric.unit === '%' ? metric.value : Math.min(100, (metric.value / 200) * 100)
+ const barClass = statusBarClass[metric.status]
+
+ return (
+
+
+ {metric.label}
+
+ {metric.value}
+ {metric.unit}
+
+
+
+
+ )
+}
diff --git a/src/mocks/dashboardData.ts b/src/mocks/dashboardData.ts
new file mode 100644
index 0000000..5879222
--- /dev/null
+++ b/src/mocks/dashboardData.ts
@@ -0,0 +1,83 @@
+export interface DashboardStats {
+ totalDispositivos: number
+ dispositivosOnline: number
+ dispositivosOffline: number
+ dispositivosAlerta: number
+ secondary?: {
+ total?: string
+ online?: string
+ offline?: string
+ alerta?: string
+ }
+}
+
+export interface DeviceStatusBreakdown {
+ online: number
+ offline: number
+ advertencia: number
+}
+
+export type RecentActivityType = 'login' | 'alert' | 'connection' | 'disconnection'
+
+export type AlertSeverity = 'INFO' | 'WARNING' | 'CRITICAL'
+
+export interface RecentActivityItem {
+ id: string
+ type: RecentActivityType
+ description: string
+ deviceName: string
+ timestamp: Date
+ severity?: AlertSeverity
+}
+
+export interface SystemHealthMetric {
+ label: string
+ value: number
+ unit: string
+ status: 'healthy' | 'warning' | 'critical'
+}
+
+export interface SystemHealth {
+ cpu: SystemHealthMetric
+ ram: SystemHealthMetric
+ network: SystemHealthMetric
+}
+
+export type QuickConnectionStatus = 'online' | 'advertencia' | 'offline'
+
+export interface QuickConnectionItem {
+ id: string
+ name: string
+ status: QuickConnectionStatus
+}
+
+export const MOCK_DASHBOARD_SECONDARY = {
+ total: '+2 este mes',
+ online: '60% disponibilidad',
+ offline: '-1 vs ayer',
+ alerta: '2 requieren atención',
+}
+
+export const MOCK_RECENT_ACTIVITY: RecentActivityItem[] = [
+ { id: '1', type: 'login', description: 'Sesión iniciada', deviceName: 'PC-ADMIN-01', timestamp: new Date(Date.now() - 1000 * 60 * 2) },
+ { id: '2', type: 'alert', description: 'CPU alta detectada', deviceName: 'LAPTOP-VENTAS', timestamp: new Date(Date.now() - 1000 * 60 * 15) },
+ { id: '3', type: 'connection', description: 'Dispositivo conectado', deviceName: 'SRV-PRINCIPAL', timestamp: new Date(Date.now() - 1000 * 60 * 32) },
+ { id: '4', type: 'disconnection', description: 'Conexión perdida', deviceName: 'SRV-BACKUP', timestamp: new Date(Date.now() - 1000 * 60 * 120) },
+ { id: '5', type: 'alert', description: 'Actualización pendiente', deviceName: 'PC-OFICINA-02', timestamp: new Date(Date.now() - 1000 * 60 * 45) },
+ { id: '6', type: 'connection', description: 'Dispositivo conectado', deviceName: 'ROUTER-PRINCIPAL', timestamp: new Date(Date.now() - 1000 * 60 * 90) },
+]
+
+export const MOCK_SYSTEM_HEALTH: SystemHealth = {
+ cpu: { label: 'CPU Promedio', value: 42, unit: '%', status: 'healthy' },
+ ram: { label: 'RAM Promedio', value: 68, unit: '%', status: 'warning' },
+ network: { label: 'Red', value: 125, unit: 'MB/s', status: 'healthy' },
+}
+
+export const MOCK_QUICK_CONNECTIONS: QuickConnectionItem[] = [
+ { id: '1', name: 'SRV-PRINCIPAL', status: 'online' },
+ { id: '2', name: 'PC-ADMIN-01', status: 'online' },
+ { id: '3', name: 'LAPTOP-VENTAS', status: 'advertencia' },
+ { id: '4', name: 'ROUTER-PRINCIPAL', status: 'online' },
+ { id: '5', name: 'SW-CORE-01', status: 'online' },
+ { id: '6', name: 'SRV-BACKUP', status: 'offline' },
+]