diff --git a/src/app/(dashboard)/archivos/page.tsx b/src/app/(dashboard)/archivos/page.tsx index 07ea440..fd4c86b 100644 --- a/src/app/(dashboard)/archivos/page.tsx +++ b/src/app/(dashboard)/archivos/page.tsx @@ -1,6 +1,7 @@ 'use client' -import { useState, useMemo, useCallback } from 'react' +import { useState, useMemo, useCallback, useEffect } from 'react' +import { useSearchParams } from 'next/navigation' import { useSelectedClient } from '@/components/providers/SelectedClientProvider' import { trpc } from '@/lib/trpc-client' import FileExplorerContainer from '@/components/files/FileExplorerContainer' @@ -51,6 +52,9 @@ export default function FileExplorerPage() { const { selectedClientId } = useSelectedClient() const clienteId = selectedClientId ?? undefined + const searchParams = useSearchParams() + const deviceIdFromUrl = searchParams.get('deviceId') + const listQuery = trpc.equipos.list.useQuery( { clienteId, limit: 100 }, { refetchOnWindowFocus: false } @@ -64,6 +68,15 @@ export default function FileExplorerPage() { const [selectedDeviceId, setSelectedDeviceId] = useState('') const [currentPath, setCurrentPath] = useState('C:\\') + useEffect(() => { + if (!deviceIdFromUrl || !listQuery.data?.dispositivos?.length) return + const exists = listQuery.data.dispositivos.some((d) => d.id === deviceIdFromUrl) + if (exists) { + setSelectedDeviceId(deviceIdFromUrl) + setCurrentPath('C:\\') + } + }, [deviceIdFromUrl, listQuery.data?.dispositivos]) + const selectedDevice = selectedDeviceId ? devices.find((d) => d.id === selectedDeviceId) : null diff --git a/src/app/(dashboard)/devices/page.tsx b/src/app/(dashboard)/devices/page.tsx index 8219d74..c6c8759 100644 --- a/src/app/(dashboard)/devices/page.tsx +++ b/src/app/(dashboard)/devices/page.tsx @@ -1,11 +1,14 @@ 'use client' import { useState, useMemo } from 'react' -import { Search } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { Search, Plus } 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' +import AddDeviceModal from '@/components/devices/AddDeviceModal' +import DeviceDetailModal from '@/components/devices/device-detail/DeviceDetailModal' type StateFilter = '' | 'ONLINE' | 'OFFLINE' | 'ALERTA' | 'MANTENIMIENTO' | 'DESCONOCIDO' @@ -61,6 +64,12 @@ export default function DevicesPage() { const [search, setSearch] = useState('') const [stateFilter, setStateFilter] = useState('') const [osFilter, setOsFilter] = useState('') + const [addModalOpen, setAddModalOpen] = useState(false) + const [detailDeviceId, setDetailDeviceId] = useState(null) + const [detailDeviceName, setDetailDeviceName] = useState('') + const [connectError, setConnectError] = useState(null) + const router = useRouter() + const utils = trpc.useUtils() const listQuery = trpc.equipos.list.useQuery( { @@ -86,23 +95,81 @@ export default function DevicesPage() { })) }, [listQuery.data]) + const openDetail = (id: string, name: string) => { + setDetailDeviceId(id) + setDetailDeviceName(name) + } + + const [connectingId, setConnectingId] = useState(null) + const iniciarSesionMutation = trpc.equipos.iniciarSesion.useMutation({ + onSuccess: (data) => { + setConnectError(null) + setConnectingId(null) + utils.sesiones.list.invalidate() + utils.clientes.dashboardStats.invalidate() + if (data.url) window.open(data.url, '_blank', 'noopener,noreferrer') + }, + onError: (err) => { + setConnectError(err.message) + setConnectingId(null) + }, + }) + const handleConnect = (id: string) => { - console.log('Conectar', id) + setConnectError(null) + setConnectingId(id) + iniciarSesionMutation.mutate({ dispositivoId: id, tipo: 'desktop' }) } const handleFiles = (id: string) => { - console.log('Archivos', id) + router.push(`/archivos?deviceId=${encodeURIComponent(id)}`) } const handleTerminal = (id: string) => { - console.log('Terminal', id) + router.push(`/terminal?deviceId=${encodeURIComponent(id)}`) } return (
-
-

Dispositivos

-

Administración de equipos conectados

+
+
+

Dispositivos

+

Administración de equipos conectados

+
+
+ {connectError && ( +
+ {connectError} + +
+ )} + + setAddModalOpen(false)} + clienteId={clienteId} + onSuccess={() => utils.equipos.list.invalidate()} + /> + + setDetailDeviceId(null)} + deviceId={detailDeviceId} + deviceName={detailDeviceName} + onConnect={handleConnect} + onTerminal={handleTerminal} + onFiles={handleFiles} + /> +
@@ -166,6 +233,8 @@ export default function DevicesPage() { onConectar={handleConnect} onArchivos={handleFiles} onTerminal={handleTerminal} + onInfo={(id, name) => openDetail(id, name ?? device.name)} + isConnecting={connectingId === device.id} /> ))}
diff --git a/src/app/(dashboard)/terminal/page.tsx b/src/app/(dashboard)/terminal/page.tsx index 847e568..24954c0 100644 --- a/src/app/(dashboard)/terminal/page.tsx +++ b/src/app/(dashboard)/terminal/page.tsx @@ -1,6 +1,7 @@ 'use client' -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect } from 'react' +import { useSearchParams } from 'next/navigation' import { useSelectedClient } from '@/components/providers/SelectedClientProvider' import { trpc } from '@/lib/trpc-client' import TerminalWindow from '@/components/terminal/TerminalWindow' @@ -25,6 +26,9 @@ export default function TerminalPage() { const { selectedClientId } = useSelectedClient() const clienteId = selectedClientId ?? undefined + const searchParams = useSearchParams() + const deviceIdFromUrl = searchParams.get('deviceId') + const listQuery = trpc.equipos.list.useQuery( { clienteId, limit: 100 }, { refetchOnWindowFocus: false } @@ -36,6 +40,12 @@ export default function TerminalPage() { })) const [selectedDeviceId, setSelectedDeviceId] = useState('') + + useEffect(() => { + if (!deviceIdFromUrl || !listQuery.data?.dispositivos?.length) return + const exists = listQuery.data.dispositivos.some((d) => d.id === deviceIdFromUrl) + if (exists) setSelectedDeviceId(deviceIdFromUrl) + }, [deviceIdFromUrl, listQuery.data?.dispositivos]) const [outputLines, setOutputLines] = useState([]) const [command, setCommand] = useState('') diff --git a/src/components/devices/AddDeviceModal.tsx b/src/components/devices/AddDeviceModal.tsx new file mode 100644 index 0000000..7b0b696 --- /dev/null +++ b/src/components/devices/AddDeviceModal.tsx @@ -0,0 +1,373 @@ +'use client' + +import { useState, useEffect } from 'react' +import { X } from 'lucide-react' +import { trpc } from '@/lib/trpc-client' +import { cn } from '@/lib/utils' + +const TIPO_OPTIONS: { value: string; label: string }[] = [ + { value: 'PC', label: 'PC' }, + { value: 'LAPTOP', label: 'Laptop' }, + { value: 'SERVIDOR', label: 'Servidor' }, + { value: 'CELULAR', label: 'Celular' }, + { value: 'TABLET', label: 'Tablet' }, + { value: 'ROUTER', label: 'Router' }, + { value: 'SWITCH', label: 'Switch' }, + { value: 'FIREWALL', label: 'Firewall' }, + { value: 'AP', label: 'Access Point' }, + { value: 'IMPRESORA', label: 'Impresora' }, + { value: 'OTRO', label: 'Otro' }, +] + +const ESTADO_OPTIONS: { value: string; label: string }[] = [ + { value: 'DESCONOCIDO', label: 'Desconocido' }, + { value: 'ONLINE', label: 'En línea' }, + { value: 'OFFLINE', label: 'Fuera de línea' }, + { value: 'ALERTA', label: 'Advertencia' }, + { value: 'MANTENIMIENTO', label: 'Mantenimiento' }, +] + +export interface AddDeviceFormValues { + tipo: string + nombre: string + descripcion: string + ubicacionId: string + estado: string + ip: string + mac: string + sistemaOperativo: string + versionSO: string + fabricante: string + modelo: string + serial: string +} + +const initialValues: AddDeviceFormValues = { + tipo: 'PC', + nombre: '', + descripcion: '', + ubicacionId: '', + estado: 'DESCONOCIDO', + ip: '', + mac: '', + sistemaOperativo: '', + versionSO: '', + fabricante: '', + modelo: '', + serial: '', +} + +interface AddDeviceModalProps { + open: boolean + onClose: () => void + clienteId: string | undefined + onSuccess?: () => void +} + +const inputClass = + 'w-full rounded-lg border border-white/10 bg-dark-300 px-4 py-2.5 text-sm text-gray-200 placeholder-gray-500 focus:border-primary-500/50 focus:outline-none focus:ring-2 focus:ring-primary-500/20' + +export default function AddDeviceModal({ + open, + onClose, + clienteId, + onSuccess, +}: AddDeviceModalProps) { + const [form, setForm] = useState(initialValues) + const [submitError, setSubmitError] = useState(null) + + const utils = trpc.useUtils() + const ubicacionesQuery = trpc.clientes.ubicaciones.list.useQuery( + { clienteId: clienteId! }, + { enabled: open && !!clienteId } + ) + const createMutation = trpc.equipos.create.useMutation({ + onSuccess: () => { + utils.equipos.list.invalidate() + utils.clientes.dashboardStats.invalidate() + onSuccess?.() + handleClose() + }, + onError: (err) => { + setSubmitError(err.message) + }, + }) + + useEffect(() => { + if (!open) { + setForm(initialValues) + setSubmitError(null) + } + }, [open]) + + const handleClose = () => { + setSubmitError(null) + onClose() + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + setSubmitError(null) + if (!clienteId) { + setSubmitError('Seleccione un cliente.') + return + } + createMutation.mutate({ + clienteId, + tipo: form.tipo as AddDeviceFormValues['tipo'], + nombre: form.nombre.trim(), + descripcion: form.descripcion.trim() || undefined, + ubicacionId: form.ubicacionId || undefined, + estado: form.estado as AddDeviceFormValues['estado'], + ip: form.ip.trim() || undefined, + mac: form.mac.trim() || undefined, + sistemaOperativo: form.sistemaOperativo.trim() || undefined, + versionSO: form.versionSO.trim() || undefined, + fabricante: form.fabricante.trim() || undefined, + modelo: form.modelo.trim() || undefined, + serial: form.serial.trim() || undefined, + }) + } + + if (!open) return null + + return ( +
+
+
+
+

+ Agregar Dispositivo +

+ +
+ +
+ {submitError && ( +
+ {submitError} +
+ )} + + {!clienteId && ( +

+ Seleccione un cliente en el selector del header para poder agregar dispositivos. +

+ )} + +
+ + +
+ +
+ + setForm((f) => ({ ...f, nombre: e.target.value }))} + className={inputClass} + placeholder="Ej: PC-Oficina-01" + required + /> +
+ +
+ +