Feat: Migrado de datos de MariaDB a SQL Server y Fix de Tabla
This commit is contained in:
		
							
								
								
									
										718
									
								
								frontend/src/components/SimpleTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										718
									
								
								frontend/src/components/SimpleTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,718 @@ | ||||
| import React, { useEffect, useRef, useState, type CSSProperties } from 'react'; | ||||
| import { | ||||
|   useReactTable, | ||||
|   getCoreRowModel, | ||||
|   getFilteredRowModel, | ||||
|   getSortedRowModel, | ||||
|   flexRender, | ||||
|   type CellContext | ||||
| } from '@tanstack/react-table'; | ||||
| import { Tooltip } from 'react-tooltip'; | ||||
| import type { Equipo, Sector, Usuario, HistorialEquipo } from '../types/interfaces'; | ||||
|  | ||||
| const SimpleTable = () => { | ||||
|   const [data, setData] = useState<Equipo[]>([]); | ||||
|   const [filteredData, setFilteredData] = useState<Equipo[]>([]); | ||||
|   const [globalFilter, setGlobalFilter] = useState(''); | ||||
|   const [selectedSector, setSelectedSector] = useState('Todos'); | ||||
|   const [modalData, setModalData] = useState<Equipo | null>(null); | ||||
|   const [sectores, setSectores] = useState<Sector[]>([]); | ||||
|   const [modalPasswordData, setModalPasswordData] = useState<Usuario | null>(null); | ||||
|   const [newPassword, setNewPassword] = useState(''); | ||||
|   const [showScrollButton, setShowScrollButton] = useState(false); | ||||
|   const [selectedEquipo, setSelectedEquipo] = useState<Equipo | null>(null); | ||||
|   const [historial, setHistorial] = useState<HistorialEquipo[]>([]); | ||||
|   const [isOnline, setIsOnline] = useState(false); | ||||
|   const passwordInputRef = useRef<HTMLInputElement>(null); | ||||
|   const BASE_URL = 'http://localhost:5198/api'; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; | ||||
|  | ||||
|     if (selectedEquipo || modalData || modalPasswordData) { | ||||
|       document.body.classList.add('scroll-lock'); | ||||
|       document.body.style.paddingRight = `${scrollBarWidth}px`; | ||||
|     } else { | ||||
|       document.body.classList.remove('scroll-lock'); | ||||
|       document.body.style.paddingRight = '0'; | ||||
|     } | ||||
|  | ||||
|     return () => { | ||||
|       document.body.classList.remove('scroll-lock'); | ||||
|       document.body.style.paddingRight = '0'; | ||||
|     }; | ||||
|   }, [selectedEquipo, modalData, modalPasswordData]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     let isMounted = true; | ||||
|  | ||||
|     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(); | ||||
|         if (isMounted) setIsOnline(data.isAlive); | ||||
|  | ||||
|       } catch (error) { | ||||
|         if (isMounted) setIsOnline(false); | ||||
|         console.error('Error checking ping:', error); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     checkPing(); | ||||
|     const interval = setInterval(checkPing, 10000); | ||||
|  | ||||
|     return () => { | ||||
|       isMounted = false; | ||||
|       clearInterval(interval); | ||||
|       setIsOnline(false); | ||||
|     }; | ||||
|   }, [selectedEquipo?.ip]); | ||||
|  | ||||
|   const handleCloseModal = () => { | ||||
|     setSelectedEquipo(null); | ||||
|     setIsOnline(false); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (selectedEquipo) { | ||||
|       fetch(`${BASE_URL}/equipos/${selectedEquipo.hostname}/historial`) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => setHistorial(data.historial)) | ||||
|         .catch(error => console.error('Error fetching history:', error)); | ||||
|     } | ||||
|   }, [selectedEquipo]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const handleScroll = () => { | ||||
|       setShowScrollButton(window.scrollY > 200); | ||||
|     }; | ||||
|  | ||||
|     window.addEventListener('scroll', handleScroll); | ||||
|     return () => window.removeEventListener('scroll', handleScroll); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (modalPasswordData && passwordInputRef.current) { | ||||
|       passwordInputRef.current.focus(); | ||||
|     } | ||||
|   }, [modalPasswordData]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetch(`${BASE_URL}/equipos`) | ||||
|       .then(response => response.json()) | ||||
|       .then((fetchedData: Equipo[]) => { | ||||
|         setData(fetchedData); | ||||
|         setFilteredData(fetchedData); | ||||
|       }); | ||||
|  | ||||
|     fetch(`${BASE_URL}/sectores`) | ||||
|       .then(response => response.json()) | ||||
|       .then((fetchedSectores: Sector[]) => { | ||||
|         const sectoresOrdenados = [...fetchedSectores].sort((a, b) => | ||||
|           a.nombre.localeCompare(b.nombre, 'es', { sensitivity: 'base' }) | ||||
|         ); | ||||
|         setSectores(sectoresOrdenados); | ||||
|       }); | ||||
|   }, []); | ||||
|  | ||||
|   const handleSectorChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | ||||
|     const selectedValue = e.target.value; | ||||
|     setSelectedSector(selectedValue); | ||||
|  | ||||
|     if (selectedValue === 'Todos') { | ||||
|       setFilteredData(data); | ||||
|     } else if (selectedValue === 'Asignar') { | ||||
|       const filtered = data.filter(item => !item.sector); | ||||
|       setFilteredData(filtered); | ||||
|     } else { | ||||
|       const filtered = data.filter(item => item.sector?.nombre === selectedValue); | ||||
|       setFilteredData(filtered); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleSave = async () => { | ||||
|     if (!modalData || !modalData.sector) return; | ||||
|  | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/${modalData.id}/sector/${modalData.sector.id}`, { | ||||
|         method: 'PATCH', | ||||
|         headers: { 'Content-Type': 'application/json' } | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) throw new Error('Error al asociar el sector'); | ||||
|  | ||||
|       // Actualizamos el dato localmente para reflejar el cambio inmediatamente | ||||
|       const updatedData = data.map(e => e.id === modalData.id ? { ...e, sector: modalData.sector } : e); | ||||
|       setData(updatedData); | ||||
|       setFilteredData(updatedData); | ||||
|  | ||||
|       setModalData(null); | ||||
|     } catch (error) { | ||||
|       console.error(error); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleSavePassword = async () => { | ||||
|     if (!modalPasswordData) return; | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/usuarios/${modalPasswordData.id}`, { | ||||
|         method: 'PUT', | ||||
|         headers: { 'Content-Type': 'application/json' }, | ||||
|         body: JSON.stringify({ password: newPassword }), | ||||
|       }); | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         const errorData = await response.json(); | ||||
|         throw new Error(errorData.error || 'Error al actualizar la contraseña'); | ||||
|       } | ||||
|  | ||||
|       const updatedUser = await response.json(); | ||||
|  | ||||
|       const updatedData = data.map(equipo => ({ | ||||
|         ...equipo, | ||||
|         usuarios: equipo.usuarios?.map(user => | ||||
|           user.id === updatedUser.id ? { ...user, password: newPassword } : user | ||||
|         ) | ||||
|       })); | ||||
|  | ||||
|       setData(updatedData); | ||||
|       setFilteredData(updatedData); | ||||
|       setModalPasswordData(null); | ||||
|       setNewPassword(''); | ||||
|  | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         console.error('Error:', error); | ||||
|         alert(error.message); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleRemoveUser = async (hostname: string, username: string) => { | ||||
|     if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return; | ||||
|  | ||||
|     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 usuario'); | ||||
|  | ||||
|       const updateFunc = (prevData: Equipo[]) => prevData.map(equipo => { | ||||
|         if (equipo.hostname === hostname) { | ||||
|           return { | ||||
|             ...equipo, | ||||
|             usuarios: equipo.usuarios.filter(u => u.username !== username), | ||||
|           }; | ||||
|         } | ||||
|         return equipo; | ||||
|       }); | ||||
|  | ||||
|       setData(updateFunc); | ||||
|       setFilteredData(updateFunc); | ||||
|  | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         console.error('Error:', error); | ||||
|         alert(error.message); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (id: number) => { | ||||
|     if (!window.confirm('¿Estás seguro de eliminar este equipo y todas sus relaciones?')) return false; | ||||
|  | ||||
|     try { | ||||
|       const response = await fetch(`${BASE_URL}/equipos/${id}`, { | ||||
|         method: 'DELETE' | ||||
|       }); | ||||
|  | ||||
|       if (response.status === 204) { | ||||
|         setData(prev => prev.filter(equipo => equipo.id !== id)); | ||||
|         setFilteredData(prev => prev.filter(equipo => equipo.id !== id)); | ||||
|         return true; | ||||
|       } | ||||
|  | ||||
|       const errorText = await response.text(); | ||||
|       throw new Error(errorText); | ||||
|  | ||||
|     } catch (error) { | ||||
|       if (error instanceof Error) { | ||||
|         console.error('Error eliminando equipo:', error); | ||||
|         alert(`Error al eliminar el equipo: ${error.message}`); | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const columns = [ | ||||
|     { header: "ID", accessorKey: "id", enableHiding: true }, | ||||
|     { | ||||
|       header: "Nombre", | ||||
|       accessorKey: "hostname", | ||||
|       cell: ({ row }: CellContext<Equipo, any>) => ( | ||||
|         <button | ||||
|           onClick={() => setSelectedEquipo(row.original)} | ||||
|           style={{ | ||||
|             background: 'none', | ||||
|             border: 'none', | ||||
|             color: '#007bff', | ||||
|             cursor: 'pointer', | ||||
|             textDecoration: 'underline', | ||||
|             padding: 0, | ||||
|             fontSize: 'inherit' | ||||
|           }} | ||||
|         > | ||||
|           {row.original.hostname} | ||||
|         </button> | ||||
|       ) | ||||
|     }, | ||||
|     { header: "IP", accessorKey: "ip" }, | ||||
|     { header: "MAC", accessorKey: "mac", enableHiding: true }, | ||||
|     { header: "Motherboard", accessorKey: "motherboard" }, | ||||
|     { header: "CPU", accessorKey: "cpu" }, | ||||
|     { header: "RAM", accessorKey: "ram_installed" }, | ||||
|     { | ||||
|       header: "Discos", | ||||
|       accessorFn: (row: Equipo) => row.discos?.length > 0 | ||||
|         ? row.discos.map(d => `${d.mediatype} ${d.size}GB`).join(" | ") | ||||
|         : "Sin discos" | ||||
|     }, | ||||
|     { header: "OS", accessorKey: "os" }, | ||||
|     { header: "Arquitectura", accessorKey: "architecture" }, | ||||
|     { | ||||
|       header: "Usuarios y Claves", | ||||
|       cell: ({ row }: CellContext<Equipo, any>) => { | ||||
|         const usuarios = row.original.usuarios || []; | ||||
|         return ( | ||||
|           <div style={{ minWidth: "240px" }}> | ||||
|             {usuarios.map((u: Usuario) => ( | ||||
|               <div | ||||
|                 key={u.id} | ||||
|                 style={{ | ||||
|                   display: "flex", | ||||
|                   alignItems: "center", | ||||
|                   justifyContent: "space-between", | ||||
|                   margin: "4px 0", | ||||
|                   padding: "6px", | ||||
|                   backgroundColor: '#f8f9fa', | ||||
|                   borderRadius: '4px', | ||||
|                   position: 'relative' | ||||
|                 }} | ||||
|               > | ||||
|                 <span style={{ color: '#495057' }}> | ||||
|                   U: {u.username} - C: {u.password || 'N/A'} | ||||
|                 </span> | ||||
|  | ||||
|                 <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}> | ||||
|                   <button | ||||
|                     onClick={() => setModalPasswordData(u)} | ||||
|                     style={tableButtonStyle} | ||||
|                     data-tooltip-id={`edit-${u.id}`} | ||||
|                   > | ||||
|                     ✏️ | ||||
|                     <Tooltip id={`edit-${u.id}`} place="top"> | ||||
|                       Cambiar contraseña | ||||
|                     </Tooltip> | ||||
|                   </button> | ||||
|  | ||||
|                   <button | ||||
|                     onClick={() => handleRemoveUser(row.original.hostname, u.username)} | ||||
|                     style={deleteUserButtonStyle} | ||||
|                     data-tooltip-id={`remove-${u.id}`} | ||||
|                   > | ||||
|                     × | ||||
|                     <Tooltip id={`remove-${u.id}`} place="top"> | ||||
|                       Quitar del equipo | ||||
|                     </Tooltip> | ||||
|                   </button> | ||||
|                 </div> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       header: "Sector", | ||||
|       id: 'sector', | ||||
|       accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar', | ||||
|       cell: ({ row }: CellContext<Equipo, any>) => { | ||||
|         const sector = row.original.sector; | ||||
|  | ||||
|         return ( | ||||
|           <div style={{ | ||||
|             display: 'flex', | ||||
|             justifyContent: 'space-between', | ||||
|             alignItems: 'center', | ||||
|             width: '100%', | ||||
|             gap: '0.5rem' | ||||
|           }}> | ||||
|             <span style={{ | ||||
|               color: sector ? '#212529' : '#6c757d', | ||||
|               fontStyle: sector ? 'normal' : 'italic', | ||||
|               flex: 1, | ||||
|               overflow: 'hidden', | ||||
|               textOverflow: 'ellipsis', | ||||
|               whiteSpace: 'nowrap' | ||||
|             }}> | ||||
|               {sector?.nombre || 'Asignar'} | ||||
|             </span> | ||||
|  | ||||
|             <button | ||||
|               onClick={() => setModalData(row.original)} | ||||
|               style={tableButtonStyle} | ||||
|               data-tooltip-id={`editSector-${row.id}`} | ||||
|             > | ||||
|               ✏️ | ||||
|               <Tooltip id={`editSector-${row.id}`} place="top"> | ||||
|                 Cambiar sector | ||||
|               </Tooltip> | ||||
|             </button> | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   ]; | ||||
|  | ||||
|   const table = useReactTable({ | ||||
|     data: filteredData, | ||||
|     columns, | ||||
|     getCoreRowModel: getCoreRowModel(), | ||||
|     getFilteredRowModel: getFilteredRowModel(), | ||||
|     getSortedRowModel: getSortedRowModel(), | ||||
|     initialState: { | ||||
|       sorting: [ | ||||
|         { id: 'sector', desc: false }, | ||||
|         { id: 'hostname', desc: false } | ||||
|       ], | ||||
|       columnVisibility: { id: false, mac: false } | ||||
|     }, | ||||
|     state: { | ||||
|       globalFilter, | ||||
|     }, | ||||
|     onGlobalFilterChange: setGlobalFilter, | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <h2>Equipos</h2> | ||||
|       <div style={{ display: 'flex', gap: '20px', marginBottom: '10px' }}> | ||||
|         <input | ||||
|           type="text" | ||||
|           placeholder="Buscar..." | ||||
|           value={globalFilter} | ||||
|           onChange={(e) => setGlobalFilter(e.target.value)} /> | ||||
|         <b>Selección de sector:</b> | ||||
|         <select value={selectedSector} onChange={handleSectorChange}> | ||||
|           <option value="Todos">-Todos-</option> | ||||
|           <option value="Asignar">-Asignar-</option> | ||||
|           {sectores.map(sector => ( | ||||
|             <option key={sector.id} value={sector.nombre}>{sector.nombre}</option> | ||||
|           ))} | ||||
|         </select> | ||||
|       </div> | ||||
|       <hr /> | ||||
|       <table style={tableStyle}> | ||||
|         <thead> | ||||
|           {table.getHeaderGroups().map(headerGroup => ( | ||||
|             <tr key={headerGroup.id}> | ||||
|               {headerGroup.headers.map(header => ( | ||||
|                 <th | ||||
|                   key={header.id} | ||||
|                   style={headerStyle} | ||||
|                   onClick={header.column.getToggleSortingHandler()}> | ||||
|                   {flexRender(header.column.columnDef.header, header.getContext())} | ||||
|                   {header.column.getIsSorted() && ( | ||||
|                     <span style={{ | ||||
|                       marginLeft: '0.5rem', | ||||
|                       fontSize: '1.2em', | ||||
|                       display: 'inline-block', | ||||
|                       transform: 'translateY(-1px)', | ||||
|                       color: '#007bff', | ||||
|                       minWidth: '20px' | ||||
|                     }}> | ||||
|                       {header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'} | ||||
|                     </span> | ||||
|                   )} | ||||
|                 </th> | ||||
|               ))} | ||||
|             </tr> | ||||
|           ))} | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           {table.getRowModel().rows.map(row => ( | ||||
|             <tr key={row.id} style={rowStyle}> | ||||
|               {row.getVisibleCells().map(cell => ( | ||||
|                 <td key={cell.id} style={cellStyle}> | ||||
|                   {flexRender(cell.column.columnDef.cell, cell.getContext())} | ||||
|                 </td> | ||||
|               ))} | ||||
|             </tr> | ||||
|           ))} | ||||
|         </tbody> | ||||
|       </table> | ||||
|  | ||||
|       {showScrollButton && ( | ||||
|         <button | ||||
|           onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} | ||||
|           style={scrollToTopStyle} | ||||
|           title="Volver arriba" | ||||
|         > | ||||
|           ↑ | ||||
|         </button> | ||||
|       )} | ||||
|  | ||||
|       {modalData && ( | ||||
|         <div style={modalStyle}> | ||||
|           <h3 style={{ margin: '0 0 1.5rem', color: '#2d3436' }}>Editar Sector</h3> | ||||
|           <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}> | ||||
|             Sector: | ||||
|             <select | ||||
|               style={inputStyle} | ||||
|               value={modalData.sector?.id || ""} | ||||
|               onChange={(e) => { | ||||
|                 const selectedId = e.target.value; | ||||
|                 const nuevoSector = selectedId === "" ? undefined : sectores.find(s => s.id === Number(selectedId)); | ||||
|                 setModalData({ ...modalData, sector: nuevoSector }); | ||||
|               }} | ||||
|             > | ||||
|               <option value="">Asignar</option> | ||||
|               {sectores.map(sector => ( | ||||
|                 <option key={sector.id} value={sector.id}>{sector.nombre}</option> | ||||
|               ))} | ||||
|             </select> | ||||
|           </label> | ||||
|  | ||||
|           <div style={{ display: 'flex', gap: '10px', marginTop: '1.5rem' }}> | ||||
|             <button | ||||
|               style={{ ...buttonStyle.base, ...(modalData.sector ? buttonStyle.primary : buttonStyle.disabled) }} | ||||
|               onClick={handleSave} | ||||
|               disabled={!modalData.sector} | ||||
|             > | ||||
|               Guardar cambios | ||||
|             </button> | ||||
|             <button | ||||
|               style={{ ...buttonStyle.base, ...buttonStyle.secondary }} | ||||
|               onClick={() => setModalData(null)} | ||||
|             > | ||||
|               Cancelar | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|       {modalPasswordData && ( | ||||
|         <div style={modalStyle}> | ||||
|           <h3 style={{ margin: '0 0 1.5rem', color: '#2d3436' }}> | ||||
|             Cambiar contraseña para {modalPasswordData.username} | ||||
|           </h3> | ||||
|           <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}> | ||||
|             Nueva contraseña: | ||||
|             <input | ||||
|               type="text" | ||||
|               value={newPassword} | ||||
|               onChange={(e) => setNewPassword(e.target.value)} | ||||
|               style={inputStyle} | ||||
|               placeholder="Ingrese la nueva contraseña" | ||||
|               ref={passwordInputRef} | ||||
|             /> | ||||
|           </label> | ||||
|           <div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}> | ||||
|             <button | ||||
|               style={{ ...buttonStyle.base, ...buttonStyle.primary }} | ||||
|               onClick={handleSavePassword} | ||||
|             > | ||||
|               Guardar | ||||
|             </button> | ||||
|             <button | ||||
|               style={{ ...buttonStyle.base, ...buttonStyle.secondary }} | ||||
|               onClick={() => { setModalPasswordData(null); setNewPassword(''); }} | ||||
|             > | ||||
|               Cancelar | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|       {selectedEquipo && ( | ||||
|         <div style={modalGrandeStyle}> | ||||
|           <button onClick={handleCloseModal} style={closeButtonStyle}> | ||||
|             × | ||||
|           </button> | ||||
|           <div style={{ maxWidth: '95vw', marginBottom: '10px', width: '100%', padding: '10px' }}> | ||||
|             <div style={modalHeaderStyle}> | ||||
|               <h2>Datos del equipo '{selectedEquipo.hostname}'</h2> | ||||
|             </div> | ||||
|             <div style={seccionStyle}> | ||||
|               <h3 style={tituloSeccionStyle}>Datos actuales</h3> | ||||
|               <div style={gridDatosStyle}> | ||||
|                 {Object.entries(selectedEquipo).map(([key, value]) => { | ||||
|                   // Omitimos claves que mostraremos de forma personalizada o no son relevantes aquí | ||||
|                   if (['id', 'usuarios', 'sector', 'discos', 'historial', 'equiposDiscos', 'memoriasRam', 'sector_id'].includes(key)) return null; | ||||
|  | ||||
|                   const formattedValue = (key === 'created_at' || key === 'updated_at') | ||||
|                     ? new Date(value as string).toLocaleString('es-ES') | ||||
|                     : (value as any)?.toString() || 'N/A'; | ||||
|  | ||||
|                   return ( | ||||
|                     <div key={key} style={datoItemStyle}> | ||||
|                       <strong style={labelStyle}>{key.replace(/_/g, ' ')}:</strong> | ||||
|                       <span style={valorStyle}>{formattedValue}</span> | ||||
|                     </div> | ||||
|                   ); | ||||
|                 })} | ||||
|  | ||||
|                 {/* --- CORRECCIÓN 1: Mostrar nombre del sector --- */} | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Sector:</strong> | ||||
|                   <span style={valorStyle}> | ||||
|                     {selectedEquipo.sector?.nombre || 'No asignado'} | ||||
|                   </span> | ||||
|                 </div> | ||||
|  | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Modulos RAM:</strong> | ||||
|                   <span style={valorStyle}> | ||||
|                     {selectedEquipo.memoriasRam?.map(m => `Slot ${m.slot}: ${m.partNumber || 'N/P'} ${m.tamano}GB ${m.velocidad || ''}MHz`).join(' | ') || 'N/A'} | ||||
|                   </span> | ||||
|                 </div> | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Discos:</strong> | ||||
|                   <span style={valorStyle}> | ||||
|                     {selectedEquipo.discos?.map(d => `${d.mediatype} ${d.size}GB`).join(', ') || 'N/A'} | ||||
|                   </span> | ||||
|                 </div> | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Usuarios:</strong> | ||||
|                   <span style={valorStyle}> | ||||
|                     {selectedEquipo.usuarios?.map(u => u.username).join(', ') || 'N/A'} | ||||
|                   </span> | ||||
|                 </div> | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Estado:</strong> | ||||
|                   <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> | ||||
|                     <div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: isOnline ? '#28a745' : '#dc3545', boxShadow: `0 0 8px ${isOnline ? '#28a74580' : '#dc354580'}` }} /> | ||||
|                     <span style={valorStyle}>{isOnline ? 'En línea' : 'Sin conexión'}</span> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Wake On Lan:</strong> | ||||
|                   <button onClick={async () => { | ||||
|                     try { | ||||
|                       await fetch(`${BASE_URL}/equipos/wake-on-lan`, { | ||||
|                         method: 'POST', | ||||
|                         headers: { 'Content-Type': 'application/json' }, | ||||
|                         body: JSON.stringify({ mac: selectedEquipo.mac, ip: selectedEquipo.ip }) | ||||
|                       }); | ||||
|                       alert('Solicitud de encendido enviada!'); | ||||
|                     } catch (error) { | ||||
|                       console.error('Error al enviar la solicitud:', error); | ||||
|                       alert('Error al enviar la solicitud'); | ||||
|                     } | ||||
|                   }} style={powerButtonStyle} data-tooltip-id="modal-power-tooltip"> | ||||
|                     <img src="./img/power.png" alt="Encender equipo" style={powerIconStyle} /> | ||||
|                     <Tooltip id="modal-power-tooltip" place="top">Encender equipo (WOL)</Tooltip> | ||||
|                   </button> | ||||
|                 </div> | ||||
|                 <div style={datoItemStyle}> | ||||
|                   <strong style={labelStyle}>Eliminar Equipo:</strong> | ||||
|                   <button onClick={async () => { | ||||
|                     const success = await handleDelete(selectedEquipo.id); | ||||
|                     if (success) handleCloseModal(); | ||||
|                   }} style={deleteButtonStyle} data-tooltip-id="modal-delete-tooltip"> | ||||
|                     × | ||||
|                     <Tooltip id="modal-delete-tooltip" place="top">Eliminar equipo permanentemente</Tooltip> | ||||
|                   </button> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div style={seccionStyle}> | ||||
|               <h3 style={tituloSeccionStyle}>Historial de cambios</h3> | ||||
|               <table style={historialTableStyle}> | ||||
|                 <thead> | ||||
|                   <tr> | ||||
|                     <th style={historialHeaderStyle}>Fecha</th> | ||||
|                     <th style={historialHeaderStyle}>Campo modificado</th> | ||||
|                     <th style={historialHeaderStyle}>Valor anterior</th> | ||||
|                     <th style={historialHeaderStyle}>Valor nuevo</th> | ||||
|                   </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                   {historial | ||||
|                     .sort((a, b) => new Date(b.fecha_cambio).getTime() - new Date(a.fecha_cambio).getTime()) | ||||
|                     .map((cambio, index) => ( | ||||
|                       <tr key={index} style={index % 2 === 0 ? historialRowStyle : { ...historialRowStyle, backgroundColor: '#f8f9fa' }}> | ||||
|                         <td style={historialCellStyle}> | ||||
|                           {new Date(cambio.fecha_cambio).toLocaleString('es-ES', { | ||||
|                             year: 'numeric', month: '2-digit', day: '2-digit', | ||||
|                             hour: '2-digit', minute: '2-digit' | ||||
|                           })} | ||||
|                         </td> | ||||
|                         <td style={historialCellStyle}>{cambio.campo_modificado}</td> | ||||
|                         <td style={historialCellStyle}>{cambio.valor_anterior}</td> | ||||
|                         <td style={historialCellStyle}>{cambio.valor_nuevo}</td> | ||||
|                       </tr> | ||||
|                     ))} | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| // --- ESTILOS --- | ||||
| const modalStyle: CSSProperties = { | ||||
|   position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff', | ||||
|   borderRadius: '12px', padding: '2rem', boxShadow: '0px 8px 30px rgba(0, 0, 0, 0.12)', zIndex: 1000, | ||||
|   minWidth: '400px', maxWidth: '90%', border: '1px solid #e0e0e0', fontFamily: 'Segoe UI, sans-serif' | ||||
| }; | ||||
| const buttonStyle = { | ||||
|   base: { padding: '8px 20px', borderRadius: '6px', border: 'none', cursor: 'pointer', transition: 'all 0.2s ease', fontWeight: '500', fontSize: '14px' } as CSSProperties, | ||||
|   primary: { backgroundColor: '#007bff', color: 'white' } as CSSProperties, | ||||
|   secondary: { backgroundColor: '#6c757d', color: 'white' } as CSSProperties, | ||||
|   disabled: { backgroundColor: '#e9ecef', color: '#6c757d', cursor: 'not-allowed' } as CSSProperties | ||||
| }; | ||||
| const inputStyle: CSSProperties = { padding: '10px', borderRadius: '6px', border: '1px solid #ced4da', width: '100%', boxSizing: 'border-box', margin: '8px 0' }; | ||||
| const tableStyle: CSSProperties = { borderCollapse: 'collapse', fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: '0.875rem', boxShadow: '0 1px 3px rgba(0,0,0,0.08)', tableLayout: 'auto', width: '100%' }; | ||||
| const headerStyle: CSSProperties = { color: '#212529', fontWeight: 600, padding: '0.75rem 1rem', borderBottom: '2px solid #dee2e6', textAlign: 'left', cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap', position: 'sticky', top: -1, zIndex: 2, backgroundColor: '#f8f9fa' }; | ||||
| const cellStyle: CSSProperties = { padding: '0.75rem 1rem', borderBottom: '1px solid #e9ecef', color: '#495057', backgroundColor: 'white' }; | ||||
| const rowStyle: CSSProperties = { transition: 'background-color 0.2s ease' }; | ||||
| const tableButtonStyle: CSSProperties = { padding: '0.375rem 0.75rem', borderRadius: '4px', border: '1px solid #dee2e6', backgroundColor: 'transparent', color: '#212529', cursor: 'pointer', transition: 'all 0.2s ease' }; | ||||
| const deleteButtonStyle: CSSProperties = { background: 'none', border: 'none', cursor: 'pointer', color: '#dc3545', fontSize: '1.5em', padding: '0 5px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' }; | ||||
| const deleteUserButtonStyle: CSSProperties = { background: 'none', border: 'none', cursor: 'pointer', color: '#dc3545', fontSize: '1.2em', padding: '0 5px', opacity: 0.7, transition: 'opacity 0.3s ease, color 0.3s ease' }; | ||||
| const scrollToTopStyle: CSSProperties = { position: 'fixed', bottom: '60px', right: '20px', width: '40px', height: '40px', borderRadius: '50%', backgroundColor: '#007bff', color: 'white', border: 'none', cursor: 'pointer', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', fontSize: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'opacity 0.3s, transform 0.3s', zIndex: 1002 }; | ||||
| const powerButtonStyle: CSSProperties = { background: 'none', border: 'none', cursor: 'pointer', padding: '5px', display: 'flex', alignItems: 'center', transition: 'transform 0.2s ease' }; | ||||
| const powerIconStyle: CSSProperties = { width: '24px', height: '24px' }; | ||||
| const modalGrandeStyle: CSSProperties = { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', backgroundColor: 'white', zIndex: 1003, overflowY: 'auto', display: 'flex', flexDirection: 'column', padding: '0px' }; | ||||
| const modalHeaderStyle: CSSProperties = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #ddd', marginBottom: '10px', top: 0, backgroundColor: 'white', zIndex: 1000, paddingTop: '5px' }; | ||||
| const tituloSeccionStyle: CSSProperties = { fontSize: '1rem', margin: '0 0 15px 0', color: '#2d3436', fontWeight: 600 }; | ||||
| const closeButtonStyle: CSSProperties = { background: 'black', color: 'white', border: 'none', borderRadius: '50%', width: '30px', height: '30px', cursor: 'pointer', fontSize: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1004, boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s', position: 'fixed', right: '30px', top: '30px' }; | ||||
| const seccionStyle: CSSProperties = { backgroundColor: '#f8f9fa', borderRadius: '8px', padding: '15px', boxShadow: '0 2px 4px rgba(0,0,0,0.05)', marginBottom: '10px' }; | ||||
| const gridDatosStyle: CSSProperties = { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '15px', marginTop: '15px' }; | ||||
| const datoItemStyle: CSSProperties = { display: 'flex', justifyContent: 'space-between', padding: '10px', backgroundColor: 'white', borderRadius: '4px', boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }; | ||||
| const labelStyle: CSSProperties = { color: '#6c757d', textTransform: 'capitalize', fontSize: '1rem', fontWeight: 800, marginRight: '8px' }; | ||||
| const valorStyle: CSSProperties = { color: '#495057', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: '0.8125rem', lineHeight: '1.4' }; | ||||
| const historialTableStyle: CSSProperties = { width: '100%', borderCollapse: 'collapse', marginTop: '15px' }; | ||||
| const historialHeaderStyle: CSSProperties = { backgroundColor: '#007bff', color: 'white', padding: '12px', textAlign: 'left', fontSize: '0.875rem' }; | ||||
| const historialCellStyle: CSSProperties = { padding: '12px', color: '#495057', fontSize: '0.8125rem' }; | ||||
| const historialRowStyle: CSSProperties = { borderBottom: '1px solid #dee2e6' }; | ||||
|  | ||||
| export default SimpleTable; | ||||
		Reference in New Issue
	
	Block a user