diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index 1887796..12e6310 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -1,11 +1,37 @@ 'use client' -import { useState, useEffect } from 'react' +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 = { @@ -142,18 +168,100 @@ const mockAlerts = [ }, ] +const DEVICES_LIMIT = 12 + export default function DashboardPage() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') - const [isRefreshing, setIsRefreshing] = useState(false) - const [stats, setStats] = useState(mockStats) - const [devices, setDevices] = useState(mockDevices) - const [alerts, setAlerts] = useState(mockAlerts) + 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 () => { - setIsRefreshing(true) - // TODO: Recargar datos de la API - await new Promise((resolve) => setTimeout(resolve, 1000)) - setIsRefreshing(false) + await Promise.all([ + statsQuery.refetch(), + alertsQuery.refetch(), + equiposQuery.refetch(), + redQuery.refetch(), + celularesQuery.refetch(), + ]) } const handleDeviceAction = (deviceId: string, action: string) => { @@ -162,17 +270,11 @@ export default function DashboardPage() { } const handleAcknowledgeAlert = (alertId: string) => { - setAlerts((prev) => - prev.map((a) => (a.id === alertId ? { ...a, estado: 'RECONOCIDA' as const } : a)) - ) - // TODO: Llamar API + acknowledgeMutation.mutate({ id: alertId }) } const handleResolveAlert = (alertId: string) => { - setAlerts((prev) => - prev.map((a) => (a.id === alertId ? { ...a, estado: 'RESUELTA' as const } : a)) - ) - // TODO: Llamar API + resolveMutation.mutate({ id: alertId }) } return ( @@ -232,20 +334,37 @@ export default function DashboardPage() { - + {devicesLoading ? ( +
+ Cargando dispositivos... +
+ ) : devices.length === 0 ? ( +
+ No hay dispositivos. Agregue clientes y sincronice con MeshCentral, LibreNMS o Headwind. +
+ ) : ( + + )} {/* Alerts */}
- + {alertsQuery.isLoading ? ( +
+ Cargando alertas... +
+ ) : ( + + )}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3b94cd6..ca6a17f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -81,6 +81,21 @@ export function getStatusBgColor(status: string): string { } } +export function getStatusBorderColor(status: string): string { + switch (status.toUpperCase()) { + case 'ONLINE': + return 'border-success/50' + case 'OFFLINE': + return 'border-gray-500/40' + case 'ALERTA': + return 'border-danger/50' + case 'MANTENIMIENTO': + return 'border-warning/50' + default: + return 'border-dark-100' + } +} + export function getSeverityColor(severity: string): string { switch (severity.toUpperCase()) { case 'CRITICAL':