refactor: dashboard page with client filter and utils (getStatusBorderColor)

This commit is contained in:
2026-02-12 15:26:24 -06:00
parent 7f6ada6d39
commit 43d2ed9011
2 changed files with 161 additions and 27 deletions

View File

@@ -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() {
</div>
</div>
<DeviceGrid
devices={devices}
viewMode={viewMode}
onAction={handleDeviceAction}
/>
{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>
<AlertsFeed
alerts={alerts}
onAcknowledge={handleAcknowledgeAlert}
onResolve={handleResolveAlert}
/>
{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>

View File

@@ -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':