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([]); const [filteredData, setFilteredData] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [selectedSector, setSelectedSector] = useState('Todos'); const [modalData, setModalData] = useState(null); const [sectores, setSectores] = useState([]); const [modalPasswordData, setModalPasswordData] = useState(null); const [newPassword, setNewPassword] = useState(''); const [showScrollButton, setShowScrollButton] = useState(false); const [selectedEquipo, setSelectedEquipo] = useState(null); const [historial, setHistorial] = useState([]); const [isOnline, setIsOnline] = useState(false); const passwordInputRef = useRef(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) => { 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) => ( ) }, { 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) => { const usuarios = row.original.usuarios || []; return (
{usuarios.map((u: Usuario) => (
U: {u.username} - C: {u.password || 'N/A'}
))}
); } }, { header: "Sector", id: 'sector', accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar', cell: ({ row }: CellContext) => { const sector = row.original.sector; return (
{sector?.nombre || 'Asignar'}
); } } ]; 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 (

Equipos

setGlobalFilter(e.target.value)} /> Selección de sector:

{table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( ))} ))} {table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( ))} ))}
{flexRender(header.column.columnDef.header, header.getContext())} {header.column.getIsSorted() && ( {header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'} )}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{showScrollButton && ( )} {modalData && (

Editar Sector

)} {modalPasswordData && (

Cambiar contraseña para {modalPasswordData.username}

)} {selectedEquipo && (

Datos del equipo '{selectedEquipo.hostname}'

Datos actuales

{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 (
{key.replace(/_/g, ' ')}: {formattedValue}
); })} {/* --- CORRECCIÓN 1: Mostrar nombre del sector --- */}
Sector: {selectedEquipo.sector?.nombre || 'No asignado'}
Modulos RAM: {selectedEquipo.memoriasRam?.map(m => `Slot ${m.slot}: ${m.partNumber || 'N/P'} ${m.tamano}GB ${m.velocidad || ''}MHz`).join(' | ') || 'N/A'}
Discos: {selectedEquipo.discos?.map(d => `${d.mediatype} ${d.size}GB`).join(', ') || 'N/A'}
Usuarios: {selectedEquipo.usuarios?.map(u => u.username).join(', ') || 'N/A'}
Estado:
{isOnline ? 'En línea' : 'Sin conexión'}
Wake On Lan:
Eliminar Equipo:

Historial de cambios

{historial .sort((a, b) => new Date(b.fecha_cambio).getTime() - new Date(a.fecha_cambio).getTime()) .map((cambio, index) => ( ))}
Fecha Campo modificado Valor anterior Valor nuevo
{new Date(cambio.fecha_cambio).toLocaleString('es-ES', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} {cambio.campo_modificado} {cambio.valor_anterior} {cambio.valor_nuevo}
)}
); }; // --- 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;