feat: devices page, DeviceCard and equipos list filter by OS
This commit is contained in:
175
src/app/(dashboard)/devices/page.tsx
Normal file
175
src/app/(dashboard)/devices/page.tsx
Normal file
@@ -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<StateFilter>('')
|
||||||
|
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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<header>
|
||||||
|
<h1 className="text-2xl font-bold text-white sm:text-3xl">Dispositivos</h1>
|
||||||
|
<p className="mt-1 text-gray-400">Administración de equipos conectados</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 sm:gap-3">
|
||||||
|
<select
|
||||||
|
value={stateFilter}
|
||||||
|
onChange={(e) => setStateFilter((e.target.value || '') as StateFilter)}
|
||||||
|
className="rounded-lg border border-white/10 bg-dark-300 px-4 py-2.5 text-sm text-gray-300 focus:border-primary-500/50 focus:outline-none focus:ring-2 focus:ring-primary-500/20 hover:border-white/20"
|
||||||
|
>
|
||||||
|
{STATE_OPTIONS.map((o) => (
|
||||||
|
<option key={o.value || 'all'} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={osFilter}
|
||||||
|
onChange={(e) => setOsFilter(e.target.value)}
|
||||||
|
className="rounded-lg border border-white/10 bg-dark-300 px-4 py-2.5 text-sm text-gray-300 focus:border-primary-500/50 focus:outline-none focus:ring-2 focus:ring-primary-500/20 hover:border-white/20"
|
||||||
|
>
|
||||||
|
{OS_OPTIONS.map((o) => (
|
||||||
|
<option key={o.value || 'all'} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{listQuery.isLoading ? (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-dark-300/50 p-12 text-center text-gray-400">
|
||||||
|
Cargando dispositivos...
|
||||||
|
</div>
|
||||||
|
) : listQuery.isError ? (
|
||||||
|
<div className="rounded-xl border border-red-500/30 bg-red-500/10 p-12 text-center text-red-400">
|
||||||
|
Error al cargar dispositivos. Intente de nuevo.
|
||||||
|
</div>
|
||||||
|
) : devices.length === 0 ? (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-dark-300/50 p-12 text-center text-gray-400">
|
||||||
|
No hay dispositivos que coincidan con los filtros.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{devices.map((device) => (
|
||||||
|
<DeviceCard
|
||||||
|
key={device.id}
|
||||||
|
id={device.id}
|
||||||
|
name={device.name}
|
||||||
|
ip={device.ip}
|
||||||
|
status={device.status}
|
||||||
|
os={device.os}
|
||||||
|
lastSeen={device.lastSeen}
|
||||||
|
onConectar={handleConnect}
|
||||||
|
onArchivos={handleFiles}
|
||||||
|
onTerminal={handleTerminal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn, formatRelativeTime, getStatusColor, getStatusBgColor } from '@/lib/utils'
|
import { cn, formatRelativeTime, getStatusColor, getStatusBgColor, getStatusBorderColor } from '@/lib/utils'
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: string
|
id: string
|
||||||
@@ -80,7 +80,7 @@ function DeviceCard({
|
|||||||
|
|
||||||
const getDeviceUrl = () => {
|
const getDeviceUrl = () => {
|
||||||
const type = device.tipo
|
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}`
|
if (['CELULAR', 'TABLET'].includes(type)) return `/celulares/${device.id}`
|
||||||
return `/red/${device.id}`
|
return `/red/${device.id}`
|
||||||
}
|
}
|
||||||
@@ -88,88 +88,75 @@ function DeviceCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'card p-4 transition-all hover:border-primary-500/50 relative group',
|
'card p-4 transition-all hover:border-primary-500/50 relative group border',
|
||||||
device.estado === 'ALERTA' && 'border-danger/50'
|
getStatusBorderColor(device.estado)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Status indicator */}
|
<div className="absolute top-3 right-3 z-10">
|
||||||
<div className="absolute top-3 right-3 flex items-center gap-2">
|
<button
|
||||||
<span
|
onClick={() => setShowMenu(!showMenu)}
|
||||||
className={cn(
|
className="p-1.5 rounded hover:bg-dark-100 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity touch-manipulation"
|
||||||
'status-dot',
|
>
|
||||||
device.estado === 'ONLINE' && 'status-dot-online',
|
<MoreVertical className="w-4 h-4 text-gray-500" />
|
||||||
device.estado === 'OFFLINE' && 'status-dot-offline',
|
</button>
|
||||||
device.estado === 'ALERTA' && 'status-dot-alert',
|
|
||||||
device.estado === 'MANTENIMIENTO' && 'status-dot-maintenance'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowMenu(!showMenu)}
|
|
||||||
className="p-1 rounded hover:bg-dark-100 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
>
|
|
||||||
<MoreVertical className="w-4 h-4 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40" onClick={() => setShowMenu(false)} />
|
<div className="fixed inset-0 z-40" onClick={() => setShowMenu(false)} />
|
||||||
<div className="dropdown right-0 z-50">
|
<div className="dropdown right-0 z-50">
|
||||||
{['PC', 'LAPTOP', 'SERVIDOR'].includes(device.tipo) && device.estado === 'ONLINE' && (
|
{['PC', 'LAPTOP', 'SERVIDOR'].includes(device.tipo) && device.estado === 'ONLINE' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAction?.(device.id, 'desktop')
|
onAction?.(device.id, 'desktop')
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}}
|
}}
|
||||||
className="dropdown-item flex items-center gap-2"
|
className="dropdown-item flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
Escritorio remoto
|
Escritorio remoto
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAction?.(device.id, 'terminal')
|
onAction?.(device.id, 'terminal')
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}}
|
}}
|
||||||
className="dropdown-item flex items-center gap-2"
|
className="dropdown-item flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Terminal className="w-4 h-4" />
|
<Terminal className="w-4 h-4" />
|
||||||
Terminal
|
Terminal
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAction?.(device.id, 'files')
|
onAction?.(device.id, 'files')
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}}
|
}}
|
||||||
className="dropdown-item flex items-center gap-2"
|
className="dropdown-item flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<FolderOpen className="w-4 h-4" />
|
<FolderOpen className="w-4 h-4" />
|
||||||
Archivos
|
Archivos
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-dark-100 my-1" />
|
<div className="h-px bg-dark-100 my-1" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onAction?.(device.id, 'restart')
|
onAction?.(device.id, 'restart')
|
||||||
setShowMenu(false)
|
setShowMenu(false)
|
||||||
}}
|
}}
|
||||||
className="dropdown-item flex items-center gap-2 text-warning"
|
className="dropdown-item flex items-center gap-2 text-warning"
|
||||||
>
|
>
|
||||||
<Power className="w-4 h-4" />
|
<Power className="w-4 h-4" />
|
||||||
Reiniciar
|
Reiniciar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Icon and name */}
|
|
||||||
<Link href={getDeviceUrl()} className="block">
|
<Link href={getDeviceUrl()} className="block">
|
||||||
<div className="flex items-center gap-4 mb-3">
|
<div className="flex items-center gap-4 mb-3">
|
||||||
<div className={cn('p-3 rounded-lg', getStatusBgColor(device.estado))}>
|
<div className={cn('p-3 rounded-lg shrink-0', getStatusBgColor(device.estado))}>
|
||||||
<span className={getStatusColor(device.estado)}>
|
<span className={getStatusColor(device.estado)}>
|
||||||
{deviceIcons[device.tipo] || deviceIcons.OTRO}
|
{deviceIcons[device.tipo] || deviceIcons.OTRO}
|
||||||
</span>
|
</span>
|
||||||
@@ -204,9 +191,9 @@ function DeviceCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics bar */}
|
{/* Metrics bar */}
|
||||||
{device.estado === 'ONLINE' && (device.cpuUsage !== null || device.ramUsage !== null) && (
|
{device.estado === 'ONLINE' && (device.cpuUsage != null || device.ramUsage != null) && (
|
||||||
<div className="mt-3 pt-3 border-t border-dark-100 grid grid-cols-2 gap-2">
|
<div className="mt-3 pt-3 border-t border-dark-100 grid grid-cols-2 gap-2">
|
||||||
{device.cpuUsage !== null && (
|
{device.cpuUsage != null && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span className="text-gray-500">CPU</span>
|
<span className="text-gray-500">CPU</span>
|
||||||
@@ -225,7 +212,7 @@ function DeviceCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{device.ramUsage !== null && (
|
{device.ramUsage != null && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span className="text-gray-500">RAM</span>
|
<span className="text-gray-500">RAM</span>
|
||||||
@@ -304,7 +291,7 @@ function DeviceList({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{device.cpuUsage !== null ? (
|
{device.cpuUsage != null ? (
|
||||||
<span className={cn(device.cpuUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
<span className={cn(device.cpuUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
||||||
{Math.round(device.cpuUsage)}%
|
{Math.round(device.cpuUsage)}%
|
||||||
</span>
|
</span>
|
||||||
@@ -313,7 +300,7 @@ function DeviceList({
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{device.ramUsage !== null ? (
|
{device.ramUsage != null ? (
|
||||||
<span className={cn(device.ramUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
<span className={cn(device.ramUsage > 80 ? 'text-danger' : 'text-gray-400')}>
|
||||||
{Math.round(device.ramUsage)}%
|
{Math.round(device.ramUsage)}%
|
||||||
</span>
|
</span>
|
||||||
@@ -326,7 +313,7 @@ function DeviceList({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Link
|
<Link
|
||||||
href={`/equipos/${device.id}`}
|
href={`/devices/${device.id}`}
|
||||||
className="btn btn-ghost btn-sm"
|
className="btn btn-ghost btn-sm"
|
||||||
>
|
>
|
||||||
Ver
|
Ver
|
||||||
|
|||||||
127
src/components/devices/DeviceCard.tsx
Normal file
127
src/components/devices/DeviceCard.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-xl border border-white/10 bg-gradient-to-b from-dark-300/80 to-dark-400/80 p-5',
|
||||||
|
'transition-all duration-200 hover:border-primary-500/30 hover:shadow-lg hover:shadow-black/20'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-dark-200 border border-white/5">
|
||||||
|
<Monitor className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-semibold text-gray-100 truncate">{name}</p>
|
||||||
|
<p className="text-sm text-gray-500 truncate">{ip || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 rounded-full px-3 py-1 text-xs font-bold',
|
||||||
|
statusStyle.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{statusStyle.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 border-t border-white/10 pt-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">{lastSeen}</span>
|
||||||
|
<span className="rounded-full bg-dark-200 px-2.5 py-0.5 text-xs text-gray-400 border border-white/5">
|
||||||
|
{osLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid grid-cols-4 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => id && onConectar?.(id)}
|
||||||
|
className="flex flex-col items-center gap-1 rounded-lg bg-dark-200/80 py-2.5 text-gray-400 transition-colors hover:bg-dark-100 hover:text-primary-400 border border-white/5"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-medium">Conectar</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => id && onArchivos?.(id)}
|
||||||
|
className="flex flex-col items-center gap-1 rounded-lg bg-dark-200/80 py-2.5 text-gray-400 transition-colors hover:bg-dark-100 hover:text-primary-400 border border-white/5"
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-medium">Archivos</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => id && onTerminal?.(id)}
|
||||||
|
className="flex flex-col items-center gap-1 rounded-lg bg-dark-200/80 py-2.5 text-gray-400 transition-colors hover:bg-dark-100 hover:text-primary-400 border border-white/5"
|
||||||
|
>
|
||||||
|
<Terminal className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-medium">Terminal</span>
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href={detailUrl}
|
||||||
|
className="flex flex-col items-center gap-1 rounded-lg bg-dark-200/80 py-2.5 text-gray-400 transition-colors hover:bg-dark-100 hover:text-primary-400 border border-white/5"
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<span className="text-xs font-medium">Info</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { TipoDispositivo } from '@prisma/client'
|
||||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||||
import { MeshCentralClient } from '@/server/services/meshcentral/client'
|
import { MeshCentralClient } from '@/server/services/meshcentral/client'
|
||||||
|
|
||||||
@@ -12,18 +13,22 @@ export const equiposRouter = router({
|
|||||||
tipo: z.enum(['PC', 'LAPTOP', 'SERVIDOR']).optional(),
|
tipo: z.enum(['PC', 'LAPTOP', 'SERVIDOR']).optional(),
|
||||||
estado: z.enum(['ONLINE', 'OFFLINE', 'ALERTA', 'MANTENIMIENTO', 'DESCONOCIDO']).optional(),
|
estado: z.enum(['ONLINE', 'OFFLINE', 'ALERTA', 'MANTENIMIENTO', 'DESCONOCIDO']).optional(),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
|
sistemaOperativo: z.string().optional(),
|
||||||
page: z.number().default(1),
|
page: z.number().default(1),
|
||||||
limit: z.number().default(20),
|
limit: z.number().default(20),
|
||||||
}).optional()
|
}).optional()
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.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 = {
|
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 } : {}),
|
...(ctx.user.clienteId ? { clienteId: ctx.user.clienteId } : {}),
|
||||||
...(clienteId ? { clienteId } : {}),
|
...(clienteId ? { clienteId } : {}),
|
||||||
...(estado ? { estado } : {}),
|
...(estado ? { estado } : {}),
|
||||||
|
...(sistemaOperativo ? {
|
||||||
|
sistemaOperativo: { contains: sistemaOperativo, mode: 'insensitive' as const },
|
||||||
|
} : {}),
|
||||||
...(search ? {
|
...(search ? {
|
||||||
OR: [
|
OR: [
|
||||||
{ nombre: { contains: search, mode: 'insensitive' as const } },
|
{ nombre: { contains: search, mode: 'insensitive' as const } },
|
||||||
|
|||||||
Reference in New Issue
Block a user