Mejoras integrales en UI, lógica de negocio y auditoría
Este commit introduce una serie de mejoras significativas en toda la aplicación, abordando la experiencia de usuario, la consistencia de los datos, la robustez del backend y la implementación de un historial de cambios completo. ✨ **Funcionalidades y Mejoras (Features & Enhancements)** * **Historial de Auditoría Completo:** * Se implementa el registro en el historial para todas las acciones CRUD manuales: creación de equipos, adición y eliminación de discos, RAM y usuarios. * Los cambios de campos simples (IP, Hostname, etc.) ahora también se registran detalladamente. * **Consistencia de Datos Mejorada:** * **RAM:** La selección de RAM en el modal de "Añadir RAM" y la vista de "Administración" ahora agrupan los módulos por especificaciones (Fabricante, Tamaño, Velocidad), eliminando las entradas duplicadas causadas por diferentes `part_number`. * **Arquitectura:** El campo de edición para la arquitectura del equipo se ha cambiado de un input de texto a un selector con las opciones fijas "32 bits" y "64 bits". * **Experiencia de Usuario (UX) Optimizada:** * El botón de "Wake On Lan" (WOL) ahora se deshabilita visualmente si el equipo no tiene una dirección MAC registrada. * Se corrige el apilamiento de modales: los sub-modales (Añadir Disco/RAM/Usuario) ahora siempre aparecen por encima del modal principal de detalles y bloquean su cierre. * El historial de cambios se actualiza en tiempo real en la interfaz después de añadir o eliminar un componente, sin necesidad de cerrar y reabrir el modal. 🐛 **Correcciones (Bug Fixes)** * **Actualización de Estado en Vivo:** Al añadir/eliminar un módulo de RAM, los campos "RAM Instalada" y "Última Actualización" ahora se recalculan en el backend y se actualizan instantáneamente en el frontend. * **Historial de Sectores Legible:** Se corrige el registro del historial para que al cambiar un sector se guarde el *nombre* del sector (ej. "Técnica") en lugar de su ID numérico. * **Formulario de Edición:** El dropdown de "Sector" en el modo de edición ahora preselecciona correctamente el sector asignado actualmente al equipo. * **Error Crítico al Añadir RAM:** Se soluciona un error del servidor (`Sequence contains more than one element`) que ocurría al añadir manualmente un tipo de RAM que ya existía con múltiples `part_number`. Se reemplazó `QuerySingleOrDefaultAsync` por `QueryFirstOrDefaultAsync` para mayor robustez. * **Eliminación Segura:** Se impide la eliminación de un sector si este tiene equipos asignados, protegiendo la integridad de los datos. ♻️ **Refactorización (Refactoring)** * **Servicio de API Centralizado:** Toda la lógica de llamadas `fetch` del frontend ha sido extraída de los componentes y centralizada en un único servicio (`apiService.ts`), mejorando drásticamente la mantenibilidad y organización del código. * **Optimización de Renders:** Se ha optimizado el rendimiento de los modales mediante el uso del hook `useCallback` para memorizar funciones que se pasan como props. * **Nulabilidad en C#:** Se han resuelto múltiples advertencias de compilación (`CS8620`) en el backend al especificar explícitamente los tipos de referencia anulables (`string?`), mejorando la seguridad de tipos del código.
This commit is contained in:
		| @@ -1,19 +1,16 @@ | ||||
| // frontend/src/components/SimpleTable.tsx | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { | ||||
|   useReactTable, | ||||
|   getCoreRowModel, | ||||
|   getFilteredRowModel, | ||||
|   getSortedRowModel, | ||||
|   getPaginationRowModel, | ||||
|   flexRender, | ||||
|   type CellContext | ||||
|   useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, | ||||
|   getPaginationRowModel, flexRender, type CellContext | ||||
| } from '@tanstack/react-table'; | ||||
| import { Tooltip } from 'react-tooltip'; | ||||
| import toast from 'react-hot-toast'; | ||||
| import type { Equipo, Sector, Usuario, UsuarioEquipoDetalle } from '../types/interfaces'; | ||||
| import styles from './SimpleTable.module.css'; | ||||
|  | ||||
| import { equipoService, sectorService, usuarioService } from '../services/apiService'; | ||||
|  | ||||
| import ModalAnadirEquipo from './ModalAnadirEquipo'; | ||||
| import ModalEditarSector from './ModalEditarSector'; | ||||
| import ModalCambiarClave from './ModalCambiarClave'; | ||||
| @@ -37,11 +34,19 @@ const SimpleTable = () => { | ||||
|   const [isAddModalOpen, setIsAddModalOpen] = useState(false); | ||||
|   const [addingComponent, setAddingComponent] = useState<'disco' | 'ram' | 'usuario' | null>(null); | ||||
|   const [isLoading, setIsLoading] = useState(true); | ||||
|   const BASE_URL = '/api'; | ||||
|  | ||||
|   const refreshHistory = async (hostname: string) => { | ||||
|     try { | ||||
|       const data = await equipoService.getHistory(hostname); | ||||
|       setHistorial(data.historial); | ||||
|     } catch (error) { | ||||
|       console.error('Error refreshing history:', error); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; | ||||
|     if (selectedEquipo || modalData || modalPasswordData) { | ||||
|     if (selectedEquipo || modalData || modalPasswordData || isAddModalOpen) { | ||||
|       document.body.classList.add('scroll-lock'); | ||||
|       document.body.style.paddingRight = `${scrollBarWidth}px`; | ||||
|     } else { | ||||
| @@ -52,7 +57,7 @@ const SimpleTable = () => { | ||||
|       document.body.classList.remove('scroll-lock'); | ||||
|       document.body.style.paddingRight = '0'; | ||||
|     }; | ||||
|   }, [selectedEquipo, modalData, modalPasswordData]); | ||||
|   }, [selectedEquipo, modalData, modalPasswordData, isAddModalOpen]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!selectedEquipo) return; | ||||
| @@ -60,17 +65,7 @@ const SimpleTable = () => { | ||||
|     const checkPing = async () => { | ||||
|       if (!selectedEquipo.ip) return; | ||||
|       try { | ||||
|         const controller = new AbortController(); | ||||
|         const timeoutId = setTimeout(() => controller.abort(), 5000); | ||||
|         const response = await fetch(`${BASE_URL}/equipos/ping`, { | ||||
|           method: 'POST', | ||||
|           headers: { 'Content-Type': 'application/json' }, | ||||
|           body: JSON.stringify({ ip: selectedEquipo.ip }), | ||||
|           signal: controller.signal | ||||
|         }); | ||||
|         clearTimeout(timeoutId); | ||||
|         if (!response.ok) throw new Error('Error en la respuesta'); | ||||
|         const data = await response.json(); | ||||
|         const data = await equipoService.ping(selectedEquipo.ip); | ||||
|         if (isMounted) setIsOnline(data.isAlive); | ||||
|       } catch (error) { | ||||
|         if (isMounted) setIsOnline(false); | ||||
| @@ -79,22 +74,21 @@ const SimpleTable = () => { | ||||
|     }; | ||||
|     checkPing(); | ||||
|     const interval = setInterval(checkPing, 10000); | ||||
|     return () => { | ||||
|       isMounted = false; | ||||
|       clearInterval(interval); | ||||
|       setIsOnline(false); | ||||
|     }; | ||||
|     return () => { isMounted = false; clearInterval(interval); setIsOnline(false); }; | ||||
|   }, [selectedEquipo]); | ||||
|  | ||||
|   const handleCloseModal = () => { | ||||
|     if (addingComponent) { | ||||
|       toast.error("Debes cerrar la ventana de añadir componente primero."); | ||||
|       return; | ||||
|     } | ||||
|     setSelectedEquipo(null); | ||||
|     setIsOnline(false); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (selectedEquipo) { | ||||
|       fetch(`${BASE_URL}/equipos/${selectedEquipo.hostname}/historial`) | ||||
|         .then(response => response.json()) | ||||
|       equipoService.getHistory(selectedEquipo.hostname) | ||||
|         .then(data => setHistorial(data.historial)) | ||||
|         .catch(error => console.error('Error fetching history:', error)); | ||||
|     } | ||||
| @@ -109,21 +103,17 @@ const SimpleTable = () => { | ||||
|   useEffect(() => { | ||||
|     setIsLoading(true); | ||||
|     Promise.all([ | ||||
|       fetch(`${BASE_URL}/equipos`).then(res => res.json()), | ||||
|       fetch(`${BASE_URL}/sectores`).then(res => res.json()) | ||||
|       equipoService.getAll(), | ||||
|       sectorService.getAll() | ||||
|     ]).then(([equiposData, sectoresData]) => { | ||||
|       setData(equiposData); | ||||
|       setFilteredData(equiposData); | ||||
|       const sectoresOrdenados = [...sectoresData].sort((a, b) => | ||||
|         a.nombre.localeCompare(b.nombre, 'es', { sensitivity: 'base' }) | ||||
|       ); | ||||
|       const sectoresOrdenados = [...sectoresData].sort((a, b) => a.nombre.localeCompare(b.nombre, 'es', { sensitivity: 'base' })); | ||||
|       setSectores(sectoresOrdenados); | ||||
|     }).catch(error => { | ||||
|       toast.error("No se pudieron cargar los datos iniciales."); | ||||
|       console.error("Error al cargar datos:", error); | ||||
|     }).finally(() => { | ||||
|       setIsLoading(false); | ||||
|     }); | ||||
|     }).finally(() => setIsLoading(false)); | ||||
|   }, []); | ||||
|  | ||||
|   const handleSectorChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | ||||
| @@ -138,16 +128,14 @@ const SimpleTable = () => { | ||||
|     if (!modalData || !modalData.sector) return; | ||||
|     const toastId = toast.loading('Guardando...'); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/${modalData.id}/sector/${modalData.sector.id}`, { method: 'PATCH' }); | ||||
|       if (!response.ok) throw new Error('Error al asociar el sector'); | ||||
|       await equipoService.updateSector(modalData.id, modalData.sector.id); | ||||
|       const updatedData = data.map(e => e.id === modalData.id ? { ...e, sector: modalData.sector } : e); | ||||
|       setData(updatedData); | ||||
|       setFilteredData(updatedData); | ||||
|       toast.success('Sector actualizado.', { id: toastId }); | ||||
|       setModalData(null); | ||||
|     } catch (error) { | ||||
|       toast.error('No se pudo actualizar.', { id: toastId }); | ||||
|       console.error(error); | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -155,16 +143,7 @@ const SimpleTable = () => { | ||||
|     if (!modalPasswordData) return; | ||||
|     const toastId = toast.loading('Actualizando...'); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/usuarios/${modalPasswordData.id}`, { | ||||
|         method: 'PUT', | ||||
|         headers: { 'Content-Type': 'application/json' }, | ||||
|         body: JSON.stringify({ password }), | ||||
|       }); | ||||
|       if (!response.ok) { | ||||
|         const err = await response.json(); | ||||
|         throw new Error(err.error || 'Error al actualizar'); | ||||
|       } | ||||
|       const updatedUser = await response.json(); | ||||
|       const updatedUser = await usuarioService.updatePassword(modalPasswordData.id, password); | ||||
|       const updatedData = data.map(equipo => ({ | ||||
|         ...equipo, | ||||
|         usuarios: equipo.usuarios?.map(user => user.id === updatedUser.id ? { ...user, password } : user) | ||||
| @@ -182,9 +161,7 @@ const SimpleTable = () => { | ||||
|     if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return; | ||||
|     const toastId = toast.loading(`Quitando a ${username}...`); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/${hostname}/usuarios/${username}`, { method: 'DELETE' }); | ||||
|       const result = await response.json(); | ||||
|       if (!response.ok) throw new Error(result.error || 'Error al desasociar'); | ||||
|       await usuarioService.removeUserFromEquipo(hostname, username); | ||||
|       const updateFunc = (prev: Equipo[]) => prev.map(e => e.hostname === hostname ? { ...e, usuarios: e.usuarios.filter(u => u.username !== username) } : e); | ||||
|       setData(updateFunc); | ||||
|       setFilteredData(updateFunc); | ||||
| @@ -198,57 +175,38 @@ const SimpleTable = () => { | ||||
|     if (!window.confirm('¿Eliminar este equipo y sus relaciones?')) return false; | ||||
|     const toastId = toast.loading('Eliminando equipo...'); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/${id}`, { method: 'DELETE' }); | ||||
|       if (response.status === 204) { | ||||
|         setData(prev => prev.filter(e => e.id !== id)); | ||||
|         setFilteredData(prev => prev.filter(e => e.id !== id)); | ||||
|         toast.success('Equipo eliminado.', { id: toastId }); | ||||
|         return true; | ||||
|       } | ||||
|       const errorText = await response.text(); | ||||
|       throw new Error(errorText || 'Error desconocido'); | ||||
|       await equipoService.deleteManual(id); | ||||
|       setData(prev => prev.filter(e => e.id !== id)); | ||||
|       setFilteredData(prev => prev.filter(e => e.id !== id)); | ||||
|       toast.success('Equipo eliminado.', { id: toastId }); | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) toast.error(`Error: ${error.message}`, { id: toastId }); | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleRemoveAssociation = async ( | ||||
|     type: 'disco' | 'ram' | 'usuario', | ||||
|     associationId: number | { equipoId: number, usuarioId: number } | ||||
|   ) => { | ||||
|  | ||||
|     let url = ''; | ||||
|     let successMessage = ''; | ||||
|  | ||||
|     if (type === 'disco' && typeof associationId === 'number') { | ||||
|       url = `${BASE_URL}/equipos/asociacion/disco/${associationId}`; | ||||
|       successMessage = 'Disco desasociado del equipo.'; | ||||
|     } else if (type === 'ram' && typeof associationId === 'number') { | ||||
|       url = `${BASE_URL}/equipos/asociacion/ram/${associationId}`; | ||||
|       successMessage = 'Módulo de RAM desasociado.'; | ||||
|     } else if (type === 'usuario' && typeof associationId === 'object') { | ||||
|       url = `${BASE_URL}/equipos/asociacion/usuario/${associationId.equipoId}/${associationId.usuarioId}`; | ||||
|       successMessage = 'Usuario desasociado del equipo.'; | ||||
|     } else { | ||||
|       return; // No hacer nada si los parámetros son incorrectos | ||||
|     } | ||||
|  | ||||
|   const handleRemoveAssociation = async (type: 'disco' | 'ram' | 'usuario', associationId: number | { equipoId: number, usuarioId: number }) => { | ||||
|     if (!window.confirm('¿Estás seguro de que quieres eliminar esta asociación manual?')) return; | ||||
|  | ||||
|     const toastId = toast.loading('Eliminando asociación...'); | ||||
|     try { | ||||
|       const response = await fetch(url, { method: 'DELETE' }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         const errorData = await response.json(); | ||||
|         throw new Error(errorData.message || `Error al eliminar la asociación.`); | ||||
|       let successMessage = ''; | ||||
|       if (type === 'disco' && typeof associationId === 'number') { | ||||
|         await equipoService.removeDiscoAssociation(associationId); | ||||
|         successMessage = 'Disco desasociado del equipo.'; | ||||
|       } else if (type === 'ram' && typeof associationId === 'number') { | ||||
|         await equipoService.removeRamAssociation(associationId); | ||||
|         successMessage = 'Módulo de RAM desasociado.'; | ||||
|       } else if (type === 'usuario' && typeof associationId === 'object') { | ||||
|         await equipoService.removeUserAssociation(associationId.equipoId, associationId.usuarioId); | ||||
|         successMessage = 'Usuario desasociado del equipo.'; | ||||
|       } else { | ||||
|         throw new Error('Tipo de asociación no válido'); | ||||
|       } | ||||
|  | ||||
|       // Actualizar el estado local para reflejar el cambio inmediatamente | ||||
|       const updateState = (prevEquipos: Equipo[]) => prevEquipos.map(equipo => { | ||||
|         if (equipo.id !== selectedEquipo?.id) return equipo; | ||||
|  | ||||
|         let updatedEquipo = { ...equipo }; | ||||
|         if (type === 'disco' && typeof associationId === 'number') { | ||||
|           updatedEquipo.discos = equipo.discos.filter(d => d.equipoDiscoId !== associationId); | ||||
| @@ -262,107 +220,75 @@ const SimpleTable = () => { | ||||
|  | ||||
|       setData(updateState); | ||||
|       setFilteredData(updateState); | ||||
|       setSelectedEquipo(prev => prev ? updateState([prev])[0] : null); // Actualiza también el modal | ||||
|       setSelectedEquipo(prev => prev ? updateState([prev])[0] : null); | ||||
|  | ||||
|       if (selectedEquipo) { | ||||
|         await refreshHistory(selectedEquipo.hostname); | ||||
|       } | ||||
|       toast.success(successMessage, { id: toastId }); | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         toast.error(error.message, { id: toastId }); | ||||
|       } | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleCreateEquipo = async (nuevoEquipo: Omit<Equipo, 'id' | 'created_at' | 'updated_at'>) => { | ||||
|     const toastId = toast.loading('Creando nuevo equipo...'); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/manual`, { | ||||
|         method: 'POST', | ||||
|         headers: { 'Content-Type': 'application/json' }, | ||||
|         body: JSON.stringify(nuevoEquipo), | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         const errorData = await response.json(); | ||||
|         throw new Error(errorData.message || 'Error al crear el equipo.'); | ||||
|       } | ||||
|  | ||||
|       const equipoCreado = await response.json(); | ||||
|  | ||||
|       // Actualizamos el estado local para ver el nuevo equipo inmediatamente | ||||
|       const equipoCreado = await equipoService.createManual(nuevoEquipo); | ||||
|       setData(prev => [...prev, equipoCreado]); | ||||
|       setFilteredData(prev => [...prev, equipoCreado]); | ||||
|  | ||||
|       toast.success(`Equipo '${equipoCreado.hostname}' creado.`, { id: toastId }); | ||||
|       setIsAddModalOpen(false); // Cerramos el modal | ||||
|       setIsAddModalOpen(false); | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         toast.error(error.message, { id: toastId }); | ||||
|       } | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleEditEquipo = async (id: number, equipoEditado: Omit<Equipo, 'id' | 'created_at' | 'updated_at'>) => { | ||||
|   const handleEditEquipo = async (id: number, equipoEditado: any) => { | ||||
|     const toastId = toast.loading('Guardando cambios...'); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/manual/${id}`, { | ||||
|         method: 'PUT', | ||||
|         headers: { 'Content-Type': 'application/json' }, | ||||
|         body: JSON.stringify(equipoEditado), | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         const errorData = await response.json(); | ||||
|         throw new Error(errorData.message || 'Error al actualizar el equipo.'); | ||||
|       } | ||||
|  | ||||
|       // Actualizar el estado local para reflejar los cambios | ||||
|       const updateState = (prevEquipos: Equipo[]) => prevEquipos.map(equipo => { | ||||
|         if (equipo.id === id) { | ||||
|           return { ...equipo, ...equipoEditado }; | ||||
|         } | ||||
|         return equipo; | ||||
|       }); | ||||
|       const equipoActualizadoDesdeBackend = await equipoService.updateManual(id, equipoEditado); | ||||
|       const updateState = (prev: Equipo[]) => | ||||
|         prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e); | ||||
|  | ||||
|       setData(updateState); | ||||
|       setFilteredData(updateState); | ||||
|       setSelectedEquipo(prev => prev ? updateState([prev])[0] : null); | ||||
|       setSelectedEquipo(equipoActualizadoDesdeBackend); | ||||
|  | ||||
|       toast.success('Equipo actualizado.', { id: toastId }); | ||||
|       return true; // Indica que el guardado fue exitoso | ||||
|       return true; | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         toast.error(error.message, { id: toastId }); | ||||
|       } | ||||
|       return false; // Indica que el guardado falló | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleAddComponent = async (type: 'disco' | 'ram' | 'usuario', data: any) => { | ||||
|   const handleAddComponent = async (type: 'disco' | 'ram' | 'usuario', componentData: any) => { | ||||
|     if (!selectedEquipo) return; | ||||
|  | ||||
|     const toastId = toast.loading(`Añadiendo ${type}...`); | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/manual/${selectedEquipo.id}/${type}`, { | ||||
|         method: 'POST', | ||||
|         headers: { 'Content-Type': 'application/json' }, | ||||
|         body: JSON.stringify(data), | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         const error = await response.json(); | ||||
|         throw new Error(error.message || `Error al añadir ${type}.`); | ||||
|       let serviceCall; | ||||
|       switch (type) { | ||||
|         case 'disco': serviceCall = equipoService.addDisco(selectedEquipo.id, componentData); break; | ||||
|         case 'ram': serviceCall = equipoService.addRam(selectedEquipo.id, componentData); break; | ||||
|         case 'usuario': serviceCall = equipoService.addUsuario(selectedEquipo.id, componentData); break; | ||||
|         default: throw new Error('Tipo de componente no válido'); | ||||
|       } | ||||
|       await serviceCall; | ||||
|  | ||||
|       // Refrescar los datos del equipo para ver el cambio | ||||
|       const refreshedEquipo = await (await fetch(`${BASE_URL}/equipos/${selectedEquipo.hostname}`)).json(); | ||||
|       // Usar el servicio directamente para obtener el equipo actualizado | ||||
|       const refreshedEquipo = await equipoService.getAll().then(equipos => equipos.find(e => e.id === selectedEquipo.id)); | ||||
|       if (!refreshedEquipo) throw new Error("No se pudo recargar el equipo"); | ||||
|  | ||||
|       const updateState = (prev: Equipo[]) => prev.map(e => e.id === selectedEquipo.id ? refreshedEquipo : e); | ||||
|       setData(updateState); | ||||
|       setFilteredData(updateState); | ||||
|       setSelectedEquipo(refreshedEquipo); | ||||
|  | ||||
|       await refreshHistory(selectedEquipo.hostname); | ||||
|  | ||||
|       toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} añadido.`, { id: toastId }); | ||||
|       setAddingComponent(null); // Cerrar modal | ||||
|       setAddingComponent(null); | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) toast.error(error.message, { id: toastId }); | ||||
|     } | ||||
| @@ -447,7 +373,7 @@ const SimpleTable = () => { | ||||
|       ], | ||||
|       columnVisibility: { id: false, mac: false }, | ||||
|       pagination: { | ||||
|         pageSize: 15, // Mostrar 15 filas por página por defecto | ||||
|         pageSize: 15, | ||||
|       }, | ||||
|     }, | ||||
|     state: { | ||||
| @@ -536,7 +462,6 @@ const SimpleTable = () => { | ||||
|         </select> | ||||
|       </div> | ||||
|  | ||||
|       {/* --- 2. Renderizar los controles ANTES de la tabla --- */} | ||||
|       {PaginacionControles} | ||||
|  | ||||
|       <div style={{ overflowX: 'auto', maxHeight: '70vh', border: '1px solid #dee2e6', borderRadius: '8px' }}> | ||||
| @@ -545,7 +470,7 @@ const SimpleTable = () => { | ||||
|             {table.getHeaderGroups().map(hg => ( | ||||
|               <tr key={hg.id}> | ||||
|                 {hg.headers.map(h => ( | ||||
|                   <th key={h.id} className={styles.th}> | ||||
|                   <th key={h.id} className={styles.th} onClick={h.column.getToggleSortingHandler()}> | ||||
|                     {flexRender(h.column.columnDef.header, h.getContext())} | ||||
|                     {h.column.getIsSorted() && (<span className={styles.sortIndicator}>{h.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}</span>)} | ||||
|                   </th> | ||||
| @@ -567,28 +492,13 @@ const SimpleTable = () => { | ||||
|         </table> | ||||
|       </div> | ||||
|  | ||||
|       {/* --- 3. Renderizar los controles DESPUÉS de la tabla --- */} | ||||
|       {PaginacionControles} | ||||
|  | ||||
|       {showScrollButton && (<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className={styles.scrollToTop} title="Volver arriba">↑</button>)} | ||||
|  | ||||
|       {modalData && ( | ||||
|         <ModalEditarSector | ||||
|           modalData={modalData} | ||||
|           setModalData={setModalData} | ||||
|           sectores={sectores} | ||||
|           onClose={() => setModalData(null)} | ||||
|           onSave={handleSave} | ||||
|         /> | ||||
|       )} | ||||
|       {modalData && <ModalEditarSector modalData={modalData} setModalData={setModalData} sectores={sectores} onClose={() => setModalData(null)} onSave={handleSave} />} | ||||
|  | ||||
|       {modalPasswordData && ( | ||||
|         <ModalCambiarClave | ||||
|           usuario={modalPasswordData} | ||||
|           onClose={() => setModalPasswordData(null)} | ||||
|           onSave={handleSavePassword} | ||||
|         /> | ||||
|       )} | ||||
|       {modalPasswordData && <ModalCambiarClave usuario={modalPasswordData} onClose={() => setModalPasswordData(null)} onSave={handleSavePassword} />} | ||||
|  | ||||
|       {selectedEquipo && ( | ||||
|         <ModalDetallesEquipo | ||||
| @@ -601,20 +511,17 @@ const SimpleTable = () => { | ||||
|           onEdit={handleEditEquipo} | ||||
|           sectores={sectores} | ||||
|           onAddComponent={type => setAddingComponent(type)} | ||||
|           isChildModalOpen={addingComponent !== null} | ||||
|         /> | ||||
|       )} | ||||
|  | ||||
|       {isAddModalOpen && ( | ||||
|         <ModalAnadirEquipo | ||||
|           sectores={sectores} | ||||
|           onClose={() => setIsAddModalOpen(false)} | ||||
|           onSave={handleCreateEquipo} | ||||
|         /> | ||||
|       )} | ||||
|       {isAddModalOpen && <ModalAnadirEquipo sectores={sectores} onClose={() => setIsAddModalOpen(false)} onSave={handleCreateEquipo} />} | ||||
|  | ||||
|       {addingComponent === 'disco' && <ModalAnadirDisco onClose={() => setAddingComponent(null)} onSave={(data) => handleAddComponent('disco', data)} />} | ||||
|       {addingComponent === 'ram' && <ModalAnadirRam onClose={() => setAddingComponent(null)} onSave={(data) => handleAddComponent('ram', data)} />} | ||||
|       {addingComponent === 'usuario' && <ModalAnadirUsuario onClose={() => setAddingComponent(null)} onSave={(data) => handleAddComponent('usuario', data)} />} | ||||
|       {addingComponent === 'disco' && <ModalAnadirDisco onClose={() => setAddingComponent(null)} onSave={(data: { mediatype: string, size: number }) => handleAddComponent('disco', data)} />} | ||||
|  | ||||
|       {addingComponent === 'ram' && <ModalAnadirRam onClose={() => setAddingComponent(null)} onSave={(data: { slot: string, tamano: number, fabricante?: string, velocidad?: number }) => handleAddComponent('ram', data)} />} | ||||
|  | ||||
|       {addingComponent === 'usuario' && <ModalAnadirUsuario onClose={() => setAddingComponent(null)} onSave={(data: { username: string }) => handleAddComponent('usuario', data)} />} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user