diff --git a/src/app/(dashboard)/devices/page.tsx b/src/app/(dashboard)/devices/page.tsx new file mode 100644 index 0000000..8219d74 --- /dev/null +++ b/src/app/(dashboard)/devices/page.tsx @@ -0,0 +1,175 @@ +'use client' + +import { useState, useMemo } from 'react' +import { Search } from 'lucide-react' +import { useSelectedClient } from '@/components/providers/SelectedClientProvider' +import { trpc } from '@/lib/trpc-client' +import { formatRelativeTime } from '@/lib/utils' +import DeviceCard, { type DeviceCardStatus } from '@/components/devices/DeviceCard' + +type StateFilter = '' | 'ONLINE' | 'OFFLINE' | 'ALERTA' | 'MANTENIMIENTO' | 'DESCONOCIDO' + +const STATE_OPTIONS: { value: StateFilter; label: string }[] = [ + { value: '', label: 'Todos los estados' }, + { value: 'ONLINE', label: 'En línea' }, + { value: 'OFFLINE', label: 'Fuera de línea' }, + { value: 'ALERTA', label: 'Advertencia' }, + { value: 'MANTENIMIENTO', label: 'Mantenimiento' }, + { value: 'DESCONOCIDO', label: 'Desconocido' }, +] + +const OS_OPTIONS: { value: string; label: string }[] = [ + { value: '', label: 'Todos los SO' }, + { value: 'Windows', label: 'Windows' }, + { value: 'Linux', label: 'Linux' }, + { value: 'Ubuntu', label: 'Ubuntu' }, + { value: 'macOS', label: 'macOS' }, +] + +function mapStateToCardStatus(state: string): DeviceCardStatus { + switch (state) { + case 'ONLINE': + return 'online' + case 'OFFLINE': + case 'DESCONOCIDO': + return 'offline' + case 'ALERTA': + case 'MANTENIMIENTO': + return 'warning' + default: + return 'offline' + } +} + +function formatLastSeenLabel(state: string, lastSeen: Date | string | null): string { + if (state === 'ONLINE') return 'En línea' + if (!lastSeen) return '—' + const d = new Date(lastSeen) + const now = new Date() + const diffMin = (now.getTime() - d.getTime()) / 60000 + if (diffMin < 1) return 'Hace un momento' + if (diffMin < 60) return `Hace ${Math.floor(diffMin)} min` + const hours = Math.floor(diffMin / 60) + if (hours < 24) return `Hace ${hours} h` + return formatRelativeTime(lastSeen) +} + +export default function DevicesPage() { + const { selectedClientId } = useSelectedClient() + const clienteId = selectedClientId ?? undefined + + const [search, setSearch] = useState('') + const [stateFilter, setStateFilter] = useState('') + const [osFilter, setOsFilter] = useState('') + + const listQuery = trpc.equipos.list.useQuery( + { + clienteId, + search: search.trim() || undefined, + estado: stateFilter || undefined, + sistemaOperativo: osFilter || undefined, + page: 1, + limit: 100, + }, + { refetchOnWindowFocus: false } + ) + + const devices = useMemo(() => { + const list = listQuery.data?.dispositivos ?? [] + return list.map((d) => ({ + id: d.id, + name: d.nombre, + ip: d.ip ?? '', + status: mapStateToCardStatus(d.estado), + os: d.sistemaOperativo ?? '—', + lastSeen: formatLastSeenLabel(d.estado, d.lastSeen), + })) + }, [listQuery.data]) + + const handleConnect = (id: string) => { + console.log('Conectar', id) + } + const handleFiles = (id: string) => { + console.log('Archivos', id) + } + const handleTerminal = (id: string) => { + console.log('Terminal', id) + } + + return ( +
+
+

Dispositivos

+

Administración de equipos conectados

+
+ +
+
+ + setSearch(e.target.value)} + placeholder="Buscar dispositivos..." + className="w-full rounded-lg border border-white/10 bg-dark-300 py-2.5 pl-10 pr-4 text-gray-200 placeholder-gray-500 focus:border-primary-500/50 focus:outline-none focus:ring-2 focus:ring-primary-500/20" + /> +
+
+ + +
+
+ + {listQuery.isLoading ? ( +
+ Cargando dispositivos... +
+ ) : listQuery.isError ? ( +
+ Error al cargar dispositivos. Intente de nuevo. +
+ ) : devices.length === 0 ? ( +
+ No hay dispositivos que coincidan con los filtros. +
+ ) : ( +
+ {devices.map((device) => ( + + ))} +
+ )} +
+ ) +} diff --git a/src/components/dashboard/DeviceGrid.tsx b/src/components/dashboard/DeviceGrid.tsx index c983377..77a3922 100644 --- a/src/components/dashboard/DeviceGrid.tsx +++ b/src/components/dashboard/DeviceGrid.tsx @@ -20,7 +20,7 @@ import { Terminal, FolderOpen, } from 'lucide-react' -import { cn, formatRelativeTime, getStatusColor, getStatusBgColor } from '@/lib/utils' +import { cn, formatRelativeTime, getStatusColor, getStatusBgColor, getStatusBorderColor } from '@/lib/utils' interface Device { id: string @@ -80,7 +80,7 @@ function DeviceCard({ const getDeviceUrl = () => { const type = device.tipo - if (['PC', 'LAPTOP', 'SERVIDOR'].includes(type)) return `/equipos/${device.id}` + if (['PC', 'LAPTOP', 'SERVIDOR'].includes(type)) return `/devices/${device.id}` if (['CELULAR', 'TABLET'].includes(type)) return `/celulares/${device.id}` return `/red/${device.id}` } @@ -88,88 +88,75 @@ function DeviceCard({ return (
- {/* Status indicator */} -
- -
- +
+ - {showMenu && ( - <> -
setShowMenu(false)} /> -
- {['PC', 'LAPTOP', 'SERVIDOR'].includes(device.tipo) && device.estado === 'ONLINE' && ( - <> - - - -
- - )} - -
- - )} -
+ {showMenu && ( + <> +
setShowMenu(false)} /> +
+ {['PC', 'LAPTOP', 'SERVIDOR'].includes(device.tipo) && device.estado === 'ONLINE' && ( + <> + + + +
+ + )} + +
+ + )}
- {/* Icon and name */}
-
+
{deviceIcons[device.tipo] || deviceIcons.OTRO} @@ -204,9 +191,9 @@ function DeviceCard({
{/* Metrics bar */} - {device.estado === 'ONLINE' && (device.cpuUsage !== null || device.ramUsage !== null) && ( + {device.estado === 'ONLINE' && (device.cpuUsage != null || device.ramUsage != null) && (
- {device.cpuUsage !== null && ( + {device.cpuUsage != null && (
CPU @@ -225,7 +212,7 @@ function DeviceCard({
)} - {device.ramUsage !== null && ( + {device.ramUsage != null && (
RAM @@ -304,7 +291,7 @@ function DeviceList({ - {device.cpuUsage !== null ? ( + {device.cpuUsage != null ? ( 80 ? 'text-danger' : 'text-gray-400')}> {Math.round(device.cpuUsage)}% @@ -313,7 +300,7 @@ function DeviceList({ )} - {device.ramUsage !== null ? ( + {device.ramUsage != null ? ( 80 ? 'text-danger' : 'text-gray-400')}> {Math.round(device.ramUsage)}% @@ -326,7 +313,7 @@ function DeviceList({ Ver diff --git a/src/components/devices/DeviceCard.tsx b/src/components/devices/DeviceCard.tsx new file mode 100644 index 0000000..bdc9145 --- /dev/null +++ b/src/components/devices/DeviceCard.tsx @@ -0,0 +1,127 @@ +'use client' + +import Link from 'next/link' +import { Monitor, FolderOpen, Terminal, Info, ExternalLink } from 'lucide-react' +import { cn } from '@/lib/utils' + +export type DeviceCardStatus = 'online' | 'offline' | 'warning' +export type DeviceCardOS = 'Windows' | 'Linux' | 'macOS' | string + +export interface DeviceCardProps { + id?: string + name: string + ip: string + status: DeviceCardStatus + os: DeviceCardOS + lastSeen: string + onConectar?: (id: string) => void + onArchivos?: (id: string) => void + onTerminal?: (id: string) => void +} + +const statusConfig: Record< + DeviceCardStatus, + { label: string; className: string } +> = { + online: { label: 'En línea', className: 'bg-emerald-600/90 text-white' }, + offline: { label: 'Fuera de línea', className: 'bg-red-600/90 text-white' }, + warning: { label: 'Advertencia', className: 'bg-amber-500/90 text-white' }, +} + +function normalizeOS(os: string): string { + const u = os.toLowerCase() + if (u.includes('windows')) return 'Windows' + if (u.includes('linux') || u.includes('ubuntu') || u.includes('debian')) return 'Linux' + if (u.includes('mac') || u.includes('darwin')) return 'macOS' + return os || '—' +} + +export default function DeviceCard({ + id, + name, + ip, + status, + os, + lastSeen, + onConectar, + onArchivos, + onTerminal, +}: DeviceCardProps) { + const statusStyle = statusConfig[status] + const osLabel = normalizeOS(os) + const detailUrl = id ? `/devices/${id}` : '#' + + return ( +
+
+
+ +
+
+
+
+

{name}

+

{ip || '—'}

+
+ + {statusStyle.label} + +
+
+
+ +
+
+ {lastSeen} + + {osLabel} + +
+
+ +
+ + + + + + Info + +
+
+ ) +} diff --git a/src/server/trpc/routers/equipos.router.ts b/src/server/trpc/routers/equipos.router.ts index cc0a138..cf87324 100644 --- a/src/server/trpc/routers/equipos.router.ts +++ b/src/server/trpc/routers/equipos.router.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { TRPCError } from '@trpc/server' +import { TipoDispositivo } from '@prisma/client' import { router, protectedProcedure, adminProcedure } from '../trpc' import { MeshCentralClient } from '@/server/services/meshcentral/client' @@ -12,18 +13,22 @@ export const equiposRouter = router({ tipo: z.enum(['PC', 'LAPTOP', 'SERVIDOR']).optional(), estado: z.enum(['ONLINE', 'OFFLINE', 'ALERTA', 'MANTENIMIENTO', 'DESCONOCIDO']).optional(), search: z.string().optional(), + sistemaOperativo: z.string().optional(), page: z.number().default(1), limit: z.number().default(20), }).optional() ) .query(async ({ ctx, input }) => { - const { clienteId, tipo, estado, search, page = 1, limit = 20 } = input || {} + const { clienteId, tipo, estado, search, sistemaOperativo, page = 1, limit = 20 } = input || {} const where = { - tipo: tipo ? { equals: tipo } : { in: ['PC', 'LAPTOP', 'SERVIDOR'] as const }, + tipo: tipo ? { equals: tipo } : { in: ['PC', 'LAPTOP', 'SERVIDOR'] as TipoDispositivo[] }, ...(ctx.user.clienteId ? { clienteId: ctx.user.clienteId } : {}), ...(clienteId ? { clienteId } : {}), ...(estado ? { estado } : {}), + ...(sistemaOperativo ? { + sistemaOperativo: { contains: sistemaOperativo, mode: 'insensitive' as const }, + } : {}), ...(search ? { OR: [ { nombre: { contains: search, mode: 'insensitive' as const } },